npm package: @jolyui/swing
Description: Lightweight, powerful data fetching for React. Simple by design, tiny by nature.
Author: Johuniq
Latest version: 1.0.0

# @jolyui/swing

<div align="center">

**Lightweight, powerful data fetching for React**

[![npm version](https://img.shields.io/npm/v/@jolyui/swing.svg)](https://www.npmjs.com/package/@jolyui/swing)
[![bundle size](https://img.shields.io/bundlephobia/minzip/@jolyui/swing)](https://bundlephobia.com/package/@jolyui/swing)
[![license](https://img.shields.io/npm/l/@jolyui/swing.svg)](https://github.com/johuniq/jolyui/blob/main/LICENSE)

</div>

---

## Why Swing?

Swing is **not another clone** of existing data fetching libraries. It's engineered from the ground up with a focus on:

- 🪶 **Tiny** - ~3KB minified + gzipped (core), ~5KB with React hooks
- ⚡ **Fast** - LRU cache, request deduplication, stale-while-revalidate
- 🎯 **Simple** - Intuitive API, minimal configuration required
- 💪 **Powerful** - Full TypeScript, Suspense, infinite scroll, mutations
- 🔌 **Flexible** - Works with or without React, tree-shakeable

## Installation

```bash
# npm
npm install @jolyui/swing

# pnpm
pnpm add @jolyui/swing

# yarn
yarn add @jolyui/swing
```

## Quick Start

### Basic Usage

```tsx
import { useSwing } from '@jolyui/swing';

function UserProfile({ userId }) {
  const { data, isLoading, error } = useSwing(`/api/users/${userId}`);

  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} />;

  return <div>{data.name}</div>;
}
```

### With Configuration

```tsx
import { useSwing } from '@jolyui/swing';

function SearchResults({ query }) {
  const { data, isLoading, refetch } = useSwing('/api/search', {
    params: { q: query },
    enabled: query.length > 0,
    cache: 'stale-while-revalidate',
    cacheTTL: 60000, // 1 minute
    revalidateOnFocus: true,
    retry: { count: 3 },
  });

  return (
    <div>
      {isLoading ? <Spinner /> : <Results items={data} />}
      <button onClick={refetch}>Refresh</button>
    </div>
  );
}
```

### Create a Client

```tsx
import { createClient, SwingProvider } from '@jolyui/swing';

// Create a configured client
const api = createClient({
  baseURL: 'https://api.example.com',
  headers: {
    'Authorization': `Bearer ${token}`,
  },
  timeout: 10000,
  retry: true,
});

// Use in your app
function App() {
  return (
    <SwingProvider 
      client={api}
      initialCache={window.__SWING_CACHE__} // SSR Hydration
    >
      <MyApp />
    </SwingProvider>
  );
}
```

### Persistence & Offline

Enable automatic cache persistence to `localStorage`:

```tsx
const api = createClient({
  persist: true,
  storageKey: 'my-app-cache', // optional
});
```

## API Reference

### Hooks

#### `useSwing<T>(url, options?)`

Main data fetching hook.

```tsx
const {
  data,        // Response data
  error,       // Error if request failed
  isLoading,   // Initial loading state
  isValidating,// Revalidating in background
  isSuccess,   // Request succeeded
  isError,     // Request failed
  isStale,     // Data is from cache and may be stale
  refetch,     // Manually trigger refetch
  mutate,      // Optimistically update data
} = useSwing<User>('/api/user', {
  // Request options
  method: 'GET',
  headers: { 'X-Custom': 'value' },
  params: { page: 1 },
  timeout: 5000,
  
  // Cache options
  cache: 'cache-first',
  cacheTTL: 300000,
  cacheKey: 'custom-key',
  tags: ['users'],
  
  // Behavior options
  enabled: true,
  keepPreviousData: false,
  revalidateOnFocus: true,
  revalidateOnReconnect: true,
  pollingInterval: 30000,
  
  // Callbacks
  onSuccess: (data) => console.log('Success!', data),
  onError: (error) => console.error('Error!', error),
  onSettled: (data, error) => console.log('Done'),
});
```

#### `useMutation<TData, TVariables>(mutationFn, options?)`

For creating, updating, deleting data.

```tsx
const {
  data,
  error,
  isLoading,
  isSuccess,
  isError,
  mutate,      // Fire and forget
  mutateAsync, // Returns promise
  reset,       // Reset state
} = useMutation(
  async (newUser: CreateUserInput) => {
    const response = await api.post<User>('/api/users', newUser);
    return response.data;
  },
  {
    onMutate: (variables) => {
      // Optimistic update
      return { previousData };
    },
    onSuccess: (data, variables, context) => {
      toast.success('User created!');
    },
    onError: (error, variables, context) => {
      // Rollback
    },
    invalidateOnSuccess: ['users'],
  }
);

// Usage
<button onClick={() => mutate({ name: 'John' })}>
  Create User
</button>
```

#### `useSwingMutation<TData, TVariables>(url, method, options?)`

Shorthand for common mutations.

```tsx
const createUser = useSwingMutation<User, CreateUserInput>(
  '/api/users',
  'POST',
  { invalidateOnSuccess: ['users'] }
);
```

#### `useInfinite<T, TPageParam>(getUrl, options?)`

For infinite scrolling and pagination.

```tsx
const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
} = useInfinite<Page, string>(
  (cursor) => `/api/posts?cursor=${cursor}`,
  {
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    initialPageParam: '',
  }
);

// Flatten pages
const posts = data?.pages.flatMap(page => page.items) ?? [];

return (
  <div>
    {posts.map(post => <Post key={post.id} {...post} />)}
    {hasNextPage && (
      <button onClick={fetchNextPage} disabled={isFetchingNextPage}>
        {isFetchingNextPage ? 'Loading...' : 'Load More'}
      </button>
    )}
  </div>
);
```

### Cache Strategies

| Strategy | Description |
|----------|-------------|
| `cache-first` | Return cache immediately, fetch in background |
| `network-first` | Try network, fallback to cache on error |
| `cache-only` | Only return cached data, never fetch |
| `network-only` | Always fetch, never use cache |
| `stale-while-revalidate` | Return stale cache, revalidate in background |

### Core Functions (React-free)

```tsx
import { swing, createClient, get, post } from '@jolyui/swing/core';

// Direct fetch
const response = await swing<User>('/api/user');

// Using client
const api = createClient({ baseURL: 'https://api.example.com' });
const users = await api.get<User[]>('/users');
const newUser = await api.post<User>('/users', { name: 'John' });

// Shorthand
const data = await get<User>('/api/user');
await post('/api/users', { name: 'John' });
```

### Utility Hooks

```tsx
import {
  useOnline,
  useFocus,
  useDebounce,
  useDebouncedCallback,
  useInterval,
} from '@jolyui/swing';

// Online status
const isOnline = useOnline();

// Document focus
const isFocused = useFocus();

// Debounced value
const debouncedSearch = useDebounce(search, 300);

// Debounced callback
const debouncedFetch = useDebouncedCallback(fetchData, 300);

// Polling
useInterval(() => refetch(), isEnabled ? 5000 : null);
```

### Cache Management

```tsx
import { invalidateCache, clearCache, getGlobalCache } from '@jolyui/swing';

// Invalidate by key
invalidateCache('/api/users');

// Invalidate by tags
invalidateCache(['users', 'admin']);

// Clear all cache
clearCache();

// Access cache directly
const cache = getGlobalCache();
cache.set('custom', createCacheEntry(data, 60000, ['custom']));

// Advanced cache features
const extendedCache = getGlobalCache();
console.log(extendedCache.stats()); // { hits, misses, sets, deletes, evictions, gcRuns }
extendedCache.gc();                  // Run garbage collection
extendedCache.prune();               // Remove all stale entries
extendedCache.startGC(60000);        // Auto GC every minute
extendedCache.stopGC();              // Stop auto GC

// Serialize/deserialize cache for persistence
import { serializeCache, deserializeCache } from '@jolyui/swing';
localStorage.setItem('cache', serializeCache());
deserializeCache(localStorage.getItem('cache'));
```

## DevTools

Swing includes optional DevTools for development debugging.

```tsx
import { SwingDevTools, enableConsoleLogging } from '@jolyui/swing';

function App() {
  return (
    <>
      <MyApp />
      {process.env.NODE_ENV === 'development' && (
        <SwingDevTools 
          position="bottom-right"  // or bottom-left, top-right, top-left
          defaultOpen={false}
          maxRequests={50}
          maxEvents={100}
        />
      )}
    </>
  );
}

// Or enable console logging
if (process.env.NODE_ENV === 'development') {
  enableConsoleLogging();
}
```

DevTools features:
- 📊 **Cache Stats** - Hits, misses, evictions, GC runs
- 🔑 **Cache Keys** - View and clear cached entries
- 📡 **Request Log** - Track all requests with timing and status
- 📝 **Event Stream** - Debug all internal events

## Event System

Subscribe to internal events for advanced use cases:

```tsx
import { emitter } from '@jolyui/swing';

// Listen to cache events
emitter.on('cache:hit', ({ key, stale }) => {
  analytics.track('cache_hit', { key, stale });
});

emitter.on('cache:miss', ({ key }) => {
  analytics.track('cache_miss', { key });
});

// Listen to request events
emitter.on('request:start', ({ url, method }) => {
  console.log(`Starting ${method} ${url}`);
});

emitter.on('request:success', ({ url, status, duration, fromCache }) => {
  metrics.recordLatency(url, duration);
});

emitter.on('request:error', ({ url, error, attempt }) => {
  console.error(`Request failed (attempt ${attempt}):`, url, error);
});

emitter.on('request:retry', ({ url, attempt, delay }) => {
  console.log(`Retrying ${url} in ${delay}ms (attempt ${attempt})`);
});

// Listen to mutation events
emitter.on('mutation:success', ({ data, variables }) => {
  console.log('Mutation succeeded', { data, variables });
});
```

## Debug Mode

Enable debug logging for development:

```tsx
import { setDebug, logger } from '@jolyui/swing';

// Enable debug mode
setDebug(true);

// Use the logger directly
logger.debug('Custom debug message');
logger.info('Info message');
logger.warn('Warning message');
logger.error('Error message');
```


## TypeScript

Swing is written in TypeScript and provides full type inference.

```tsx
interface User {
  id: number;
  name: string;
  email: string;
}

// Type is inferred
const { data } = useSwing<User>('/api/user');
//     ^? User | undefined

// With mutation
const createUser = useMutation<User, { name: string }>(
  (input) => api.post<User>('/users', input).then(r => r.data)
);
```

## Why Swing Over Others?

### The Problem with Existing Solutions

**TanStack Query (React Query)** is incredibly powerful, but:
- 🐘 **Heavy** - ~13KB min+gzip, plus requires a QueryClient provider
- 📚 **Complex** - Steep learning curve with QueryClient, QueryCache, queryKey arrays
- ⚙️ **Over-engineered** - Most apps don't need devtools, persistence, or offline sync
- 🔧 **Boilerplate** - Requires wrapping your app, configuring clients, defining query keys

**SWR** is lighter, but:
- 🔗 **React-locked** - No way to use the core fetching logic outside React
- 🎯 **Limited mutations** - Mutation support feels like an afterthought
- 📦 **Still heavy for simple needs** - ~4KB for basic data fetching

### Swing's Philosophy: Simplicity Without Sacrifice

```tsx
// TanStack Query - Lots of setup
const queryClient = new QueryClient();
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyComponent />
    </QueryClientProvider>
  );
}
function MyComponent() {
  const { data } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
  });
}

// SWR - Simpler but still needs fetcher
const fetcher = (url) => fetch(url).then(r => r.json());
function MyComponent() {
  const { data } = useSWR(`/api/users/${userId}`, fetcher);
}

// Swing - Just works™
function MyComponent() {
  const { data } = useSwing(`/api/users/${userId}`);
}
```

### Key Differentiators

| Aspect | Swing | TanStack Query | SWR |
|--------|-------|----------------|-----|
| **Setup Required** | None | QueryClient + Provider | Global config or fetcher |
| **Bundle Impact** | ~3KB core, ~5KB with hooks | ~13KB + devtools | ~4.5KB |
| **Learning Curve** | 5 minutes | 1-2 hours | 30 minutes |
| **Query Keys** | Automatic from URL | Manual arrays required | Automatic from URL |
| **Built-in Fetch** | ✅ Yes | ❌ Bring your own | ❌ Bring your own |
| **React Optional** | ✅ Use core anywhere | ❌ React only | ❌ React only |
| **Interceptors** | ✅ Request & response | ❌ External | ❌ External |
| **Type Inference** | ✅ Automatic | ⚠️ Needs generics | ⚠️ Needs generics |

### When to Use What

**Choose Swing if you:**
- Want the simplest possible API
- Care about bundle size
- Need to use fetching logic outside React (Node.js, Web Workers, etc.)
- Don't need offline persistence or complex cache invalidation patterns
- Want batteries-included (fetch, caching, retries) without config

**Choose TanStack Query if you:**
- Need devtools for debugging complex cache states
- Require offline-first with persistence
- Have very complex cache invalidation requirements
- Don't mind the bundle size and setup complexity

**Choose SWR if you:**
- Are already in the Vercel ecosystem
- Want something between Swing and TanStack Query
- Need Next.js-specific optimizations

### Bundle Size Comparison

```
@jolyui/swing/core    ~1.5KB  (fetch only, no React)
@jolyui/swing         ~3KB    (with React hooks)
swr                   ~4.5KB
@tanstack/react-query ~13KB   (+ devtools ~15KB more)
```

### Real-World Example

Here's a complete CRUD app with Swing vs TanStack Query:

```tsx
// ============ SWING ============
import { useSwing, useSwingMutation } from '@jolyui/swing';

function UserList() {
  const { data: users, isLoading } = useSwing<User[]>('/api/users');
  
  const deleteUser = useSwingMutation<void, number>(
    '/api/users',
    'DELETE',
    { invalidateOnSuccess: ['/api/users'] }
  );

  if (isLoading) return <Spinner />;
  
  return users.map(user => (
    <div key={user.id}>
      {user.name}
      <button onClick={() => deleteUser.mutate(user.id)}>Delete</button>
    </div>
  ));
}

// That's it. No providers, no configuration.

// ============ TANSTACK QUERY ============
import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <UserList />
    </QueryClientProvider>
  );
}

function UserList() {
  const queryClient = useQueryClient();
  
  const { data: users, isLoading } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(r => r.json()),
  });
  
  const deleteUser = useMutation({
    mutationFn: (id: number) => fetch(`/api/users/${id}`, { method: 'DELETE' }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });

  if (isLoading) return <Spinner />;
  
  return users.map(user => (
    <div key={user.id}>
      {user.name}
      <button onClick={() => deleteUser.mutate(user.id)}>Delete</button>
    </div>
  ));
}
```

**Lines of code:** Swing ~15, TanStack Query ~35

---

## Feature Comparison Table

| Feature | Swing | SWR | TanStack Query |
|---------|-------|-----|----------------|
| Bundle Size | ~3KB | ~4.5KB | ~13KB |
| Zero Config | ✅ | ✅ | ⚠️ |
| TypeScript | ✅ | ✅ | ✅ |
| Suspense | ✅ | ✅ | ✅ |
| SSR | ✅ | ✅ | ✅ |
| Devtools | ✅ | ❌ | ✅ |
| React-free Core | ✅ | ❌ | ❌ |
| Built-in Fetch | ✅ | ❌ | ❌ |
| Interceptors | ✅ | ❌ | ❌ |
| Event System | ✅ | ❌ | ⚠️ |
| Debug Mode | ✅ | ❌ | ✅ |
| Automatic Retries | ✅ | ✅ | ✅ |
| Retry Jitter | ✅ | ❌ | ⚠️ |
| Request Deduplication | ✅ | ✅ | ✅ |
| Stale-While-Revalidate | ✅ | ✅ | ✅ |
| Infinite Queries | ✅ | ✅ | ✅ |
| Mutations | ✅ | ⚠️ | ✅ |
| Optimistic Updates | ✅ | ✅ | ✅ |
| Cache Statistics | ✅ | ❌ | ⚠️ |
| Cache GC | ✅ | ❌ | ✅ |
| Cache Serialization | ✅ | ❌ | ✅ |
| Offline Support | ⚠️ | ⚠️ | ✅ |
| Persistence | ✅ | ❌ | ✅ |
| Parallel Queries | ✅ | ✅ | ✅ |
| Dependent Queries | ✅ | ✅ | ✅ |
| Prefetching | ✅ | ✅ | ✅ |
| Polling | ✅ | ✅ | ✅ |
| AbortController | ✅ | ✅ | ✅ |
| Memory Limits | ✅ | ❌ | ⚠️ |
| Middleware System | ✅ | ❌ | ❌ |
| Request Batching | ✅ | ❌ | ❌ |
| API Presets | ✅ | ❌ | ❌ |
| DataLoader Pattern | ✅ | ❌ | ❌ |

## Advanced Features

### Middleware System

Add pluggable middleware for cross-cutting concerns:

```tsx
import {
  createMiddlewareManager,
  loggingMiddleware,
  authMiddleware,
  metricsMiddleware,
  getGlobalMiddleware,
} from '@jolyui/swing';

// Create middleware manager
const middleware = createMiddlewareManager();

// Add logging
middleware.use(loggingMiddleware({ logRequest: true, logResponse: true }));

// Add authentication
middleware.use(authMiddleware(() => localStorage.getItem('token'), {
  scheme: 'Bearer',
}));

// Add metrics collection
middleware.use(metricsMiddleware((metrics) => {
  analytics.track('api_request', metrics);
}));

// Chaining on client
api.use(loggingMiddleware())
   .use(authMiddleware(() => token));

// Custom middleware
middleware.use(async (context, next) => {
  console.log('Before request:', context.url);
  const response = await next();
  console.log('After request:', response.status);
  return response;
});
```

Built-in middleware:
- `loggingMiddleware` - Log requests and responses
- `authMiddleware` - Auto-inject auth headers
- `timeoutMiddleware` - Set default timeouts
- `retryMiddleware` - Custom retry logic
- `cacheMiddleware` - Custom caching
- `transformMiddleware` - Transform requests/responses
- `errorHandlerMiddleware` - Global error handling
- `requestIdMiddleware` - Add unique request IDs
- `metricsMiddleware` - Collect performance metrics

### Request Batching

Batch multiple requests for efficiency:

```tsx
import { createBatcher, createDataLoader } from '@jolyui/swing';

// Simple batcher
const batcher = createBatcher({
  maxBatchSize: 10,
  batchDelay: 50,
});

// These will be batched together
const [user1, user2, user3] = await Promise.all([
  batcher.add('/api/users/1'),
  batcher.add('/api/users/2'),
  batcher.add('/api/users/3'),
]);

// DataLoader pattern (like Facebook's DataLoader)
const userLoader = createDataLoader({
  batchFn: async (ids) => {
    const response = await swing('/api/users', {
      params: { ids: ids.join(',') }
    });
    return response.data;
  },
  maxBatchSize: 100,
  cache: true,
});

// Automatically batched into single request
const [alice, bob] = await Promise.all([
  userLoader.load(1),
  userLoader.load(2),
]);
```

### API Presets

Pre-configured clients for common API patterns:

```tsx
import {
  createRESTClient,
  createGraphQLClient,
  createJSONAPIClient,
  createOAuth2Client,
} from '@jolyui/swing';

// REST API
const api = createRESTClient({
  baseURL: 'https://api.example.com',
  apiVersion: 'v1',
});
const users = await api.get('/users');

// GraphQL
const gql = createGraphQLClient({
  endpoint: 'https://api.example.com/graphql',
});
const result = await gql.query({
  query: `query GetUser($id: ID!) { user(id: $id) { name } }`,
  variables: { id: '1' },
});

// JSON:API (Drupal, Rails, etc.)
const jsonApi = createJSONAPIClient({
  baseURL: 'https://api.example.com',
  defaultInclude: ['author', 'comments'],
});

// OAuth2 Authentication
const oauth = createOAuth2Client({
  tokenEndpoint: 'https://auth.example.com/oauth/token',
  clientId: 'your-client-id',
});
const tokens = await oauth.exchangeCode(code, redirectUri);
const authApi = oauth.createAuthenticatedClient({ baseURL: 'https://api.example.com' });
```

### Offline Support

Queue requests when offline and sync when back online:

```tsx
import { createOfflineQueue, createOfflineAwareClient } from '@jolyui/swing';

// Basic offline queue
const offlineQueue = createOfflineQueue({}, {
  persist: true, // Persist to localStorage
  maxAttempts: 3,
  onSync: (req, res) => console.log('Synced:', req.url),
  onFail: (req, err) => console.error('Failed:', req.url, err),
});

// Add requests when offline
if (!offlineQueue.isOnline()) {
  offlineQueue.add('/api/comments', {
    method: 'POST',
    body: { text: 'Hello!' },
  });
}

// Start auto-sync when online
offlineQueue.start();

// Or use the offline-aware client
const { request, queue } = createOfflineAwareClient({
  baseURL: 'https://api.example.com',
});

// Automatically queues mutations when offline
await request('/api/posts', { method: 'POST', body: { title: 'New Post' } });
```

### Better Error Messages

Get actionable error messages with hints:

```tsx
import {
  isSwingError,
  getErrorHints,
  getUserFriendlyMessage,
  formatErrorForConsole,
} from '@jolyui/swing';

try {
  await swing('/api/users');
} catch (error) {
  if (isSwingError(error)) {
    // User-friendly message
    console.log(getUserFriendlyMessage(error));
    // "Unauthorized (401)"

    // Actionable hints
    console.log(getErrorHints(error));
    // ["Check authentication token", "Token may have expired", "Login again"]

    // Detailed console output
    console.log(formatErrorForConsole(error));
    // 🔴 Swing Error: HTTP_ERROR
    //    Message: Unauthorized: Invalid token
    //    Status: 401 (Unauthorized)
    //    Request: GET /api/users
    //
    //    💡 Hints:
    //       • Check authentication token
    //       • Token may have expired
    //       • Login again
  }
}
```

## License

MIT © [Johuniq](https://github.com/johuniq)