npm package: @iron-stack/sync
Description: Declarative real-time data sync engine -- Socket.IO events automatically update your TanStack Query cache. Define rules once, share them between server and client.
Latest version: 1.0.0

# @iron-stack/sync

Declarative real-time data sync engine -- Socket.IO events automatically update your TanStack Query cache. Define rules once, share them between server and client.

## Installation

```bash
npm install @iron-stack/sync
```

## Quick Start

### 1. Define sync rules (shared)

```ts
import { defineSyncRules } from '@iron-stack/sync';

const syncRules = defineSyncRules<{
  'message:new': Message;
  'message:updated': Message;
}>({
  'message:new': {
    room: (msg) => `conversation:${msg.conversationId}`,
    actions: [
      {
        procedure: ['message', 'list'],
        action: 'direct',
        infinite: true,
        match: (msg, input) => input?.conversationId === msg.conversationId,
        updater: (old, msg) => prependToFirstPage(old, msg),
      },
      {
        procedure: ['conversation', 'list'],
        action: 'invalidate',
      },
    ],
  },
  'message:updated': {
    room: (msg) => `conversation:${msg.conversationId}`,
    actions: [
      { procedure: ['message', 'list'], action: 'invalidate' },
    ],
  },
});
```

### 2. Server -- broadcast events

```ts
import { SyncServer } from '@iron-stack/sync/server';

const sync = new SyncServer({ io, rules: syncRules });

// Broadcast to the room defined in rules
sync.broadcast('message:new', newMessage);

// Auto-join users to rooms on connect
io.on('connection', async (socket) => {
  await sync.autoJoinRooms(socket, async (userId) => {
    const ids = await getUserConversationIds(userId);
    return ids.map(id => `conversation:${id}`);
  });
});
```

### 3. Client -- automatic cache updates

```tsx
import { SyncProvider } from '@iron-stack/sync/client';

function App() {
  return (
    <SyncProvider rules={syncRules} socket={socket}>
      <YourApp />
    </SyncProvider>
  );
}
```

When `message:new` arrives via Socket.IO, the message list cache updates instantly -- no HTTP refetch needed.

## API Reference

### `@iron-stack/sync` (shared)

| Export | Description |
|---|---|
| `defineSyncRules(rules)` | Type-safe factory for defining sync rules shared between server and client |
| `SyncRules` | Type for the complete set of sync rules |
| `SyncRule` | Type for a single event-to-cache mapping |
| `CacheAction` | Type for a cache update action (`direct`, `invalidate`, or `remove`) |

### `@iron-stack/sync/server`

| Export | Description |
|---|---|
| `SyncServer` | Server-side sync engine that manages rooms and broadcasts events |
| `SyncServer.broadcast(event, data)` | Broadcast an event to the room defined in sync rules |
| `SyncServer.joinRoom(room, userIds)` | Join all connected sockets for given user IDs to a room |
| `SyncServer.autoJoinRooms(socket, resolver)` | Auto-join a socket to rooms returned by the resolver function |
| `SyncServer.getIO()` | Access the underlying Socket.IO server instance |
| `SyncServerOptions` | Configuration: `{ io: Server, rules: SyncRules }` |

### `@iron-stack/sync/client`

| Export | Description |
|---|---|
| `SyncProvider` | React provider that connects Socket.IO events to TanStack Query cache |
| `useSync()` | Hook returning `{ connected: boolean, socket }` |
| `createSyncEngine(queryClient, socket, rules)` | Low-level engine for non-React usage. Returns a cleanup function |
| `SyncProviderProps` | Props: `{ rules, socket, children }` |

## Cache Action Types

| Action | Behavior |
|---|---|
| `"direct"` | Updates cache in-place via `setQueryData` -- fastest, no HTTP request |
| `"invalidate"` | Marks query as stale, triggers refetch from server -- simplest, always consistent |
| `"remove"` | Removes the query from cache entirely |

## Configuration

### `CacheAction` Options

| Option | Type | Description |
|---|---|---|
| `procedure` | `string[]` | tRPC procedure path, e.g. `['message', 'list']` |
| `action` | `"direct" \| "invalidate" \| "remove"` | Cache update strategy |
| `updater` | `(old, event) => new` | For `"direct"`: function that produces new cache value |
| `match` | `(event, queryInput) => boolean` | Only apply when condition matches (e.g., same conversation) |
| `infinite` | `boolean` | Whether this targets an infinite query |

## License

MIT