npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

torch-watch-history-sdk

v1.0.0

Published

Dead-simple SDK for tracking video watch history with WebSocket, auto-reconnection, and offline support

Readme

🎬 Watch History SDK

Dead-simple SDK for tracking video watch history with WebSocket, auto-reconnection, and offline support.

Built with ❤️ using Context7 best practices, Socket.IO, and TypeScript.


✨ Features

  • Insanely Easy to Use - Just call onTimeUpdate() in your video player
  • Automatic Reconnection - Handles disconnects with exponential backoff
  • Offline Queue - Saves progress even when offline, syncs when back online
  • Multi-Device Sync - Progress syncs across all user's devices in real-time
  • TypeScript First - Complete type safety with IntelliSense
  • React Hooks - Dead-simple hooks for React and React Native
  • Flutter Support - Native Dart SDK for Flutter apps
  • Zero Configuration - Works out of the box with smart defaults
  • Performance Optimized - Sub-10ms response times, batched updates
  • Network Aware (RN) - Detects network changes on React Native
  • AsyncStorage Fallback - Persists progress locally on mobile

📦 Installation

JavaScript/TypeScript (React, React Native, Vanilla)

# NPM
npm install @torch/watch-history-sdk socket.io-client

# Yarn
yarn add @torch/watch-history-sdk socket.io-client

# PNPM
pnpm add @torch/watch-history-sdk socket.io-client

React Native Additional Deps

yarn add @react-native-async-storage/async-storage @react-native-community/netinfo

Flutter

# pubspec.yaml
dependencies:
  watch_history_sdk:
    git:
      url: https://github.com/TORCH-Corp/torch-streaming-backend
      path: packages/watch-history-sdk-flutter

See Flutter SDK Documentation for complete Flutter guide.


🚀 Quick Start

React (Web)

import { WatchHistoryProvider, useVideoProgress } from '@torch/watch-history-sdk/react';

// 1. Wrap your app
function App() {
  return (
    <WatchHistoryProvider
      config={{
        url: 'https://api.torch.com',
        auth: { token: sessionToken, profileId: 'profile-123' }
      }}
    >
      <VideoPlayer />
    </WatchHistoryProvider>
  );
}

// 2. Use in your video player - that's it!
function VideoPlayer() {
  const { onTimeUpdate, watchPercentage } = useVideoProgress({
    videoContentId: 'video-123',
    autoResume: true
  });

  return (
    <video
      src={videoUrl}
      onTimeUpdate={(e) => onTimeUpdate(e.currentTarget.currentTime)}
    />
  );
}

React Native

import { useVideoProgress } from '@torch/watch-history-sdk/react-native';
import Video from 'react-native-video';

function VideoPlayer({ videoId, videoUrl }) {
  const videoRef = useRef<Video>(null);
  const { onTimeUpdate, resumeFromLastPosition } = useVideoProgress({
    videoContentId: videoId,
    autoResume: true
  });

  useEffect(() => {
    resumeFromLastPosition().then(position => {
      videoRef.current?.seek(position);
    });
  }, []);

  return (
    <Video
      ref={videoRef}
      source={{ uri: videoUrl }}
      onProgress={({ currentTime }) => onTimeUpdate(currentTime)}
    />
  );
}

Vanilla JavaScript

import { WatchHistorySDK } from '@torch/watch-history-sdk';

const sdk = new WatchHistorySDK({
  url: 'https://api.torch.com',
  auth: { token: 'your-token', profileId: 'profile-123' }
});

// Track progress
const video = document.querySelector('video');
video.addEventListener('timeupdate', () => {
  sdk.trackProgress('video-123', video.currentTime);
});

// Listen for events
sdk.on('ack', (response) => {
  console.log('Progress saved:', response.watchPercentage);
});

Flutter

import 'package:watch_history_sdk/watch_history_sdk.dart';
import 'package:video_player/video_player.dart';

// 1. Wrap your app
void main() {
  runApp(
    WatchHistoryProvider(
      config: WatchHistoryConfig(
        url: 'https://api.torch.com',
        auth: AuthConfig(token: 'your-token', profileId: 'profile-123'),
      ),
      child: MyApp(),
    ),
  );
}

// 2. Use in your video player
class VideoPlayerWidget extends StatefulWidget {
  @override
  Widget build(BuildContext context) {
    final client = WatchHistoryProvider.of(context);

    _controller.addListener(() {
      if (_controller.value.isPlaying) {
        client.trackProgress(
          'video-123',
          _controller.value.position.inSeconds.toDouble(),
        );
      }
    });

    return VideoPlayer(_controller);
  }
}

See Flutter SDK Documentation for complete guide.


📖 API Reference

React Hooks

useVideoProgress(options)

The main hook for tracking video progress. Handles everything automatically!

Options:

{
  videoContentId: string;      // Video ID (required)
  autoTrack?: boolean;          // Auto-track progress (default: true)
  autoResume?: boolean;         // Auto-resume from last position (default: true)
  updateInterval?: number;      // Update interval in seconds (default: 10)
  completionThreshold?: number; // Completion threshold % (default: 95)
}

Returns:

{
  // Progress state
  watchPercentage: number;      // 0-100
  isCompleted: boolean;
  lastPosition: number;         // In seconds
  isTracking: boolean;
  isLoading: boolean;
  error: WatchErrorEvent | null;

  // Methods
  onTimeUpdate: (currentTime: number) => void;  // Call in video player
  stopTracking: () => void;
  resumeFromLastPosition: () => Promise<number>;
  markCompleted: () => void;
}

Example:

function VideoPlayer() {
  const {
    onTimeUpdate,
    resumeFromLastPosition,
    watchPercentage,
    isCompleted
  } = useVideoProgress({
    videoContentId: 'video-123',
    autoResume: true,
    completionThreshold: 90  // Mark complete at 90%
  });

  const videoRef = useRef<HTMLVideoElement>(null);

  useEffect(() => {
    resumeFromLastPosition().then(position => {
      if (videoRef.current) {
        videoRef.current.currentTime = position;
      }
    });
  }, []);

  return (
    <div>
      <video
        ref={videoRef}
        onTimeUpdate={(e) => onTimeUpdate(e.currentTarget.currentTime)}
      />
      <ProgressBar value={watchPercentage} />
      {isCompleted && <CompletionBadge />}
    </div>
  );
}

useWatchHistory(config?)

Access the SDK instance and reactive state.

Returns:

{
  sdk: WatchHistorySDK;
  state: SDKState;
  trackProgress: (videoId: string, time: number) => void;
  markCompleted: (videoId: string) => void;
  pause: (videoId: string, time: number) => void;
  resume: (videoId: string) => Promise<WatchResumeResponse | null>;
  connect: () => void;
  disconnect: () => void;
  isConnected: boolean;
  isReconnecting: boolean;
  error: WatchErrorEvent | null;
}

useConnectionState()

Monitor connection status.

Returns:

{
  isConnected: boolean;
  isReconnecting: boolean;
  connectionState: 'connected' | 'connecting' | 'disconnected' | 'reconnecting' | 'error';
  error: WatchErrorEvent | null;
}

Example:

function ConnectionStatus() {
  const { isConnected, isReconnecting } = useConnectionState();

  return (
    <div>
      {isReconnecting && <Spinner />}
      {!isConnected && <OfflineIndicator />}
    </div>
  );
}

useWatchSync(videoId, callback)

Handle multi-device sync events.

Example:

function VideoPlayer({ videoId }) {
  const videoRef = useRef<HTMLVideoElement>(null);

  useWatchSync(videoId, (event) => {
    // Another device updated progress - sync it!
    if (videoRef.current) {
      videoRef.current.currentTime = event.currentTime;
      console.log(`Synced from ${event.device}`);
    }
  });

  return <video ref={videoRef} />;
}

Core SDK API

Constructor

const sdk = new WatchHistorySDK({
  url: string;                      // WebSocket server URL
  auth: {
    token: string;                   // Session token
    profileId?: string;              // Profile ID (optional)
  };
  autoConnect?: boolean;             // Default: true
  reconnection?: boolean;            // Default: true
  reconnectionDelay?: number;        // Default: 1000ms
  reconnectionDelayMax?: number;     // Default: 5000ms
  progressUpdateInterval?: number;   // Default: 10s
  enableOfflineQueue?: boolean;      // Default: true
  maxOfflineQueueSize?: number;      // Default: 100
  debug?: boolean;                   // Default: false
});

Methods

trackProgress(videoId, currentTime)

Track watch progress.

sdk.trackProgress('video-123', 45.5);
startAutoTracking(videoId, getCurrentTime)

Auto-track progress at configured interval.

sdk.startAutoTracking('video-123', () => video.currentTime);
markCompleted(videoId)

Mark video as completed.

sdk.markCompleted('video-123');
pause(videoId, currentTime)

Pause tracking.

sdk.pause('video-123', 45.5);
resume(videoId)

Get resume position.

const state = await sdk.resume('video-123');
video.currentTime = state.lastWatchedDuration;
setProfile(profileId)

Change profile.

sdk.setProfile('profile-456');
connect() / disconnect()

Manual connection control.

sdk.connect();
sdk.disconnect();

Events

Listen to SDK events:

// Connection events
sdk.on('connected', (event) => console.log('Connected!'));
sdk.on('disconnected', (reason) => console.log('Disconnected:', reason));
sdk.on('reconnecting', (attempt) => console.log(`Reconnecting... #${attempt}`));
sdk.on('reconnected', (attempt) => console.log('Reconnected!'));

// Watch events
sdk.on('ack', (response) => {
  console.log('Progress saved:', response.watchPercentage);
});

sdk.on('sync', (event) => {
  console.log('Synced from another device:', event.currentTime);
});

sdk.on('error', (error) => {
  console.error('Error:', error.message);
});

// State changes
sdk.on('stateChange', (state) => {
  console.log('State changed:', state);
});

🎯 Advanced Examples

Complete Video Player Component

import React, { useRef, useEffect, useState } from 'react';
import { useVideoProgress, useConnectionState, useWatchSync } from '@torch/watch-history-sdk/react';

function AdvancedVideoPlayer({ videoId, videoUrl }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [isPlaying, setIsPlaying] = useState(false);

  const {
    onTimeUpdate,
    stopTracking,
    resumeFromLastPosition,
    watchPercentage,
    isCompleted,
    markCompleted
  } = useVideoProgress({
    videoContentId: videoId,
    autoResume: true,
    completionThreshold: 95
  });

  const { isConnected, isReconnecting } = useConnectionState();

  // Multi-device sync
  useWatchSync(videoId, (event) => {
    if (videoRef.current && !isPlaying) {
      videoRef.current.currentTime = event.currentTime;
    }
  });

  // Auto-resume on mount
  useEffect(() => {
    resumeFromLastPosition().then(position => {
      if (videoRef.current && position > 0) {
        videoRef.current.currentTime = position;
      }
    });
  }, [videoId]);

  // Handle pause
  const handlePause = () => {
    setIsPlaying(false);
    if (videoRef.current) {
      stopTracking();
    }
  };

  // Handle play
  const handlePlay = () => {
    setIsPlaying(true);
  };

  // Manual complete button
  const handleMarkComplete = () => {
    markCompleted();
  };

  return (
    <div className="video-player">
      {/* Connection indicator */}
      <div className="status-bar">
        {isReconnecting && <span>🔄 Reconnecting...</span>}
        {!isConnected && <span>📡 Offline - Progress will sync when reconnected</span>}
      </div>

      {/* Video element */}
      <video
        ref={videoRef}
        src={videoUrl}
        controls
        onTimeUpdate={(e) => {
          if (isPlaying) {
            onTimeUpdate(e.currentTarget.currentTime);
          }
        }}
        onPause={handlePause}
        onPlay={handlePlay}
      />

      {/* Progress indicator */}
      <div className="progress-bar">
        <div className="fill" style={{ width: `${watchPercentage}%` }} />
      </div>

      <div className="info">
        <p>Progress: {watchPercentage.toFixed(1)}%</p>
        {isCompleted && <p>✅ Completed!</p>}
        {!isCompleted && <button onClick={handleMarkComplete}>Mark as Watched</button>}
      </div>
    </div>
  );
}

React Native with Expo AV

import { Video, AVPlaybackStatus } from 'expo-av';
import { useVideoProgress } from '@torch/watch-history-sdk/react-native';

function ExpoVideoPlayer({ videoId, videoUrl }) {
  const videoRef = useRef<Video>(null);

  const {
    onTimeUpdate,
    resumeFromLastPosition,
    watchPercentage,
    isCompleted
  } = useVideoProgress({
    videoContentId: videoId,
    autoResume: true
  });

  useEffect(() => {
    resumeFromLastPosition().then(async (position) => {
      if (videoRef.current && position > 0) {
        await videoRef.current.setPositionAsync(position * 1000); // Convert to ms
      }
    });
  }, [videoId]);

  const handlePlaybackStatusUpdate = (status: AVPlaybackStatus) => {
    if (status.isLoaded && status.isPlaying) {
      onTimeUpdate(status.positionMillis / 1000); // Convert to seconds
    }
  };

  return (
    <View>
      <Video
        ref={videoRef}
        source={{ uri: videoUrl }}
        useNativeControls
        resizeMode="contain"
        onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
      />
      <Text>Progress: {watchPercentage.toFixed(1)}%</Text>
      {isCompleted && <Badge text="Completed!" />}
    </View>
  );
}

Offline Support Indicator (React Native)

import { useOfflineQueue, useConnectionState } from '@torch/watch-history-sdk/react-native';

function OfflineSyncIndicator() {
  const { queueSize, isSyncing } = useOfflineQueue();
  const { isNetworkAvailable, isConnected } = useConnectionState();

  if (!isNetworkAvailable) {
    return (
      <View style={styles.indicator}>
        <Text>📶 No internet - {queueSize} updates queued</Text>
      </View>
    );
  }

  if (isSyncing) {
    return (
      <View style={styles.indicator}>
        <ActivityIndicator />
        <Text>Syncing {queueSize} updates...</Text>
      </View>
    );
  }

  if (!isConnected) {
    return (
      <View style={styles.indicator}>
        <Text>🔄 Connecting...</Text>
      </View>
    );
  }

  return null;
}

🔧 Configuration

Default Configuration

{
  autoConnect: true,
  reconnection: true,
  reconnectionDelay: 1000,        // 1 second
  reconnectionDelayMax: 5000,     // 5 seconds max
  reconnectionAttempts: Infinity, // Never give up!
  progressUpdateInterval: 10,     // Update every 10 seconds
  enableOfflineQueue: true,
  maxOfflineQueueSize: 100,
  debug: false
}

Custom Configuration

const sdk = new WatchHistorySDK({
  url: 'https://api.torch.com',
  auth: { token: 'token', profileId: 'profile-123' },

  // Custom intervals
  progressUpdateInterval: 5,      // Update every 5 seconds

  // Aggressive reconnection
  reconnectionDelay: 500,         // Start with 500ms
  reconnectionDelayMax: 2000,     // Max 2 seconds

  // Large offline queue
  maxOfflineQueueSize: 500,

  // Debug mode
  debug: true,

  // Custom logger
  logger: (level, message, ...args) => {
    console.log(`[${level}]`, message, ...args);
  }
});

🎓 Best Practices

1. Use Provider Pattern (React)

// ✅ Good - Single instance, shared across app
<WatchHistoryProvider config={config}>
  <App />
</WatchHistoryProvider>

// ❌ Bad - Creates new instance per component
function VideoPlayer() {
  const { trackProgress } = useWatchHistory(config); // Creates new SDK!
}

2. Handle Resume Properly

// ✅ Good - Resume on mount, let user decide to play
useEffect(() => {
  resumeFromLastPosition().then(position => {
    if (position > 0) {
      videoRef.current.currentTime = position;
      // Don't auto-play, let user click play
    }
  });
}, [videoId]);

// ❌ Bad - Auto-play on resume
useEffect(() => {
  resumeFromLastPosition().then(position => {
    videoRef.current.currentTime = position;
    videoRef.current.play(); // Annoying!
  });
}, [videoId]);

3. Show Connection State

// ✅ Good - User knows what's happening
const { isConnected } = useConnectionState();

return (
  <div>
    {!isConnected && <Banner>Offline - progress will sync later</Banner>}
    <VideoPlayer />
  </div>
);

4. Cleanup on Unmount

// ✅ Good - Cleanup automatically handled by hooks
// No need to manually cleanup when using hooks!

// For vanilla SDK:
useEffect(() => {
  return () => sdk.destroy(); // Cleanup
}, []);

🐛 Troubleshooting

Progress Not Saving

Problem: Progress updates not being sent.

Solution:

// Make sure you're calling onTimeUpdate()
<video onTimeUpdate={(e) => onTimeUpdate(e.currentTarget.currentTime)} />

// Check connection state
const { isConnected } = useConnectionState();
console.log('Connected:', isConnected);

// Enable debug mode
const sdk = new WatchHistorySDK({ ..., debug: true });

Resume Not Working

Problem: Video not resuming from last position.

Solution:

// Make sure autoResume is enabled
const { resumeFromLastPosition } = useVideoProgress({
  videoContentId: videoId,
  autoResume: true  // ← Important!
});

// Or manually call resume
useEffect(() => {
  resumeFromLastPosition().then(position => {
    console.log('Resume position:', position);
    videoRef.current.currentTime = position;
  });
}, [videoId]);

Reconnection Issues

Problem: SDK not reconnecting after disconnect.

Solution:

// Check reconnection config
const sdk = new WatchHistorySDK({
  reconnection: true,           // ← Must be true
  reconnectionAttempts: Infinity
});

// Monitor reconnection events
sdk.on('reconnecting', (attempt) => {
  console.log(`Reconnecting... attempt #${attempt}`);
});

📊 Performance

  • Sub-10ms response times (Phase 5 optimization)
  • 85% reduction in database writes (batching)
  • 95% cache hit rate for video metadata
  • Offline queue prevents data loss
  • Automatic deduplication of rapid updates
  • Smart batching reduces network usage

🤝 Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines.


📄 License

MIT © TORCH Team


🙏 Credits

Built with:


📞 Support


Made with ❤️ by the TORCH Team