OddSockets Svelte SDK

Official Svelte SDK with reactive stores and components for OddSockets real-time messaging

npm ready Reactive Stores SvelteKit Ready High Performance JavaScript SDK Core

Overview & Features

The OddSockets Svelte SDK provides reactive stores and components built on top of the JavaScript SDK, offering seamless integration with Svelte's reactive system for automatic UI updates.

Reactive Stores

Automatic UI updates with Svelte's reactive system. Messages, presence, and connection state update automatically.

SvelteKit Ready

Full SSR and hydration support with proper client-side initialization and cleanup.

JavaScript SDK Core

Built on the JavaScript SDK as single source of truth for all WebSocket functionality.

High Performance

Optimized reactive updates with efficient store subscriptions and automatic cleanup.

Pre-built Components

Ready-to-use Svelte components for common real-time messaging patterns.

Automatic Cleanup

Built-in lifecycle management with automatic cleanup on component destroy.

Installation

bash
npm install oddsockets-svelte-sdk
bash
yarn add oddsockets-svelte-sdk
bash
pnpm add oddsockets-svelte-sdk

Quick Start

Basic Chat with Reactive Stores

svelte
<script>
  import { createChannelStore } from 'oddsockets-svelte-sdk/stores';
  import { onMount } from 'svelte';
  
  const { messages, presence, publish, subscribe, unsubscribe } = createChannelStore('chat', {
    apiKey: 'ak_your_api_key_here',
    userId: 'user123'
  });
  
  let messageText = '';
  
  onMount(() => {
    subscribe();
    return unsubscribe; // Cleanup on destroy
  });
  
  async function sendMessage() {
    if (messageText.trim()) {
      await publish({
        type: 'chat',
        text: messageText,
        username: 'John Doe'
      });
      messageText = '';
    }
  }
</script>

<!-- Reactive UI updates automatically -->
<div class="chat-container">
  <div class="messages">
    {#each $messages as message}
      <div class="message">
        <strong>{message.data.username}:</strong>
        {message.data.text}
      </div>
    {/each}
  </div>
  
  <div class="presence">
    Online: {$presence.length} users
  </div>
  
  <input 
    bind:value={messageText} 
    placeholder="Type a message..."
    on:keydown={(e) => e.key === 'Enter' && sendMessage()}
  />
  <button on:click={sendMessage}>Send</button>
</div>

Connection Status

svelte
<script>
  import { createConnectionStore } from 'oddsockets-svelte-sdk/stores';

  const { connectionState, isConnected, reconnectAttempts } = createConnectionStore({
    apiKey: 'ak_your_api_key_here'
  });
</script>

<div class="status {$connectionState}">{$connectionState}</div>

{#if $isConnected}
  <span>Live</span>
{:else if $reconnectAttempts > 0}
  <span>Reconnecting (attempt {$reconnectAttempts})</span>
{/if}

Reactive Stores

The Svelte SDK's core strength is its reactive stores — subscribe once and let Svelte update the UI automatically as messages, presence and connection state change.

Channel Store

svelte
<script>
  import { createChannelStore } from 'oddsockets-svelte-sdk/stores';

  const {
    messages, presence, messageCount, presenceCount, latestMessage,
    publish, subscribe, unsubscribe
  } = createChannelStore('my-channel', { apiKey: 'ak_your_api_key_here' });
</script>

<div>Messages: {$messageCount} · Online: {$presenceCount}</div>

{#if $latestMessage}
  <div>Latest: {$latestMessage.data.text}</div>
{/if}

Multi-Channel Store

svelte
<script>
  import { createMultiChannelStore } from 'oddsockets-svelte-sdk/stores';

  const { channels, allMessages, totalPresence } = createMultiChannelStore(
    ['chat', 'notifications', 'updates'],
    { apiKey: 'ak_your_api_key_here' }
  );
</script>

<div>Total users: {$totalPresence}</div>

{#each $allMessages as message}
  <div data-channel="{message.channel}">{message.data.text}</div>
{/each}

Examples

Explore complete working examples in the examples/ directory of the repository:

Enhanced Features

Beyond core pub/sub, OddSockets ships a Slack-like enhanced surface — reactions, typing indicators, threads, read receipts, presence/status, notifications, DMs, channel management, message editing and search. It lives on client.enhanced, where client is the instance returned by createOddSocketsClient. Send an action with a client.enhanced.* method; receive the paired broadcast with client.on('<event>', handler) — the worker forwards every enhanced broadcast onto the client event surface.

Typing & Reactions

javascript
import { createOddSocketsClient } from 'oddsockets-svelte-sdk';

const client = createOddSocketsClient({ apiKey: 'ak_your_api_key_here', userId: 'alice' });
const channel = client.channel('room-42');
await channel.subscribe(() => {}, { enablePresence: true });

// Receive-path: broadcasts from other users on the channel
client.on('user_typing',    (data) => console.log(`${data.userId} is typing`));
client.on('reaction_added', (data) => console.log(`${data.userId} reacted ${data.emoji}`));

// Send-path: enhanced actions over the live socket (positional args)
client.enhanced.startTyping('alice', 'room-42');
client.enhanced.addReaction('msg-1', 'room-42', ':thumbsup:', 'alice', 'Alice');

Threads

javascript
client.on('thread_reply', (data) => console.log('new thread reply', data));

const reply = await client.enhanced.threadReply(
  'room-42', 'msg-1', 'Replying in the thread', 'alice', 'Alice'
);

// Query methods return a Promise that resolves with the worker response
const thread = await client.enhanced.getThread('thread-1');

Each area exposes methods on client.enhanced; the worker broadcasts the paired events which you handle with client.on(...):

  • TypingstartTyping, stopTypinguser_typing, user_stopped_typing
  • ReactionsaddReaction, removeReaction, getReactionsreaction_added, reaction_removed
  • ThreadsthreadReply, getThread, subscribeThread, followThread, markThreadReadthread_reply, thread_subscribed, thread_followed, thread_read_updated
  • Read receiptsmarkRead, markAllRead, getUnreadCountsuser_read, unread_count_updated, all_marked_read
  • MessageseditMessage, deleteMessage, pinMessage, unpinMessage, getPinnedMessagesmessage_edited, message_deleted, message_pinned, message_unpinned
  • Presence & statussetStatus, setCustomStatus, setDND, getUserPresenceuser_status_changed, custom_status_updated, dnd_status_changed
  • ChannelscreateChannel, updateChannel, archiveChannel, inviteToChannel, joinChannel, leaveChannelchannel_created, channel_updated, user_invited, user_joined_channel, user_left_channel
  • DMscreateDM, sendDM, getDMConversationsdm_created, dm_received
  • NotificationssubscribeNotifications, getNotifications, markNotificationRead, clearNotificationsnotification, notification_read, notifications_cleared
  • SearchsearchMessages, searchInChannel, searchByUser, filterMessages (results returned via Promise)

For any worker event not wrapped above, subscribe with the raw client.on('<event>', handler) API — all enhanced broadcasts are forwarded onto the client surface.

Challenges & Leaderboards

Challenges, leaderboards and achievements build on the same live socket. The send side lives on the enhanced surface (client.enhanced.*); request/query methods return a Promise that resolves with the worker's reply, while progress and achievement calls are fire-and-forget. Inbound broadcasts arrive on the client event surface — subscribe with the client's normal client.on('<event>', handler).

javascript
// Create a leaderboard-style challenge (awaits challenge_create_success)
const { challengeId } = await client.enhanced.createChallenge({
  name: 'Weekend Sprint', metric: 'points', scope: 'room-42'
});

// Fire-and-forget metric progress (no ack)
client.enhanced.reportProgress({ challengeId, userId: 'alice', value: 120 });

// Request top-N + the caller's rank (awaits challenge_standings_success)
const standings = await client.enhanced.getStandings({ challengeId, top: 10 });

// Finalize with an outcome (awaits challenge_complete_success)
await client.enhanced.completeChallenge({ challengeId, userId: 'alice', outcome: 'completed' });

Send-path methods

Each method is called on client.enhanced. Methods marked await resolve with the worker's reply on the named ack event; the rest are fire-and-forget over the live socket.

  • createChallenge (await) — create a challenge/leaderboard → ack challenge_create_success
  • reportProgress — fire-and-forget metric progress (no ack)
  • completeChallenge (await) — finalize with an outcome → ack challenge_complete_success
  • unlockAchievement — fire-and-forget; pass percentComplete (0–100) (no ack)
  • getStandings (await) — request top-N + caller rank → ack challenge_standings_success
  • getAchievements (await) — query achievement state → ack achievement_state
  • sendChallengeInvite (await) — directed invite to another user → ack challenge_invite_success
  • replyChallengeInvite (await) — accept/decline an invite → ack challenge_reply_success
  • cancelChallengeInvite (await) — cancel a sent invite → ack challenge_invite_cancel_success
  • getChallengeInvites (await) — list pending invites → ack challenge_invites

Outcome vocabulary

The outcome passed to completeChallenge is one of:

  • completed — win (rank 1)
  • failed — loss
  • tied — draw
  • conceded — resign / concede
  • expired — timed out

Progressive achievements

unlockAchievement always emits the wire event achievement_unlock; the worker is authoritative and derives the outbound broadcast from percentComplete. A value < 100 broadcasts achievement_progress (status in_progress); >= 100 or an omitted value broadcasts achievement_unlock (status unlocked). You never emit achievement_progress yourself.

Inbound events

Subscribe to these with client.on('<event>', handler). Room broadcasts fan out to everyone in scope; directed events are delivered only to the targeted user.

  • Room broadcastschallenge_progress, leaderboard_rank_change, challenge_complete, achievement_unlock, achievement_progress
  • Directed (per-user)challenge_invited, challenge_reply_received, challenge_invite_cancelled

Performance

Keep reactive rendering fast by capping the messages you render and keying each loop.

Efficient Rendering

svelte
<script>
  import { createChannelStore } from 'oddsockets-svelte-sdk/stores';

  const { messages } = createChannelStore('chat', { apiKey: 'ak_your_api_key_here' });

  // Only render the most recent messages for smooth updates
  $: recentMessages = $messages.slice(-50);
</script>

{#each recentMessages as message (message.id)}
  <div class="message">{message.data.text}</div>
{/each}
  • Low latency real-time delivery over a genuine Socket.IO connection.
  • 32KB message size limit for predictable performance.
  • Automatic reconnection with exponential backoff and session stickiness.
  • Bulk publishing via client.publishBulk(messages) for high-throughput fan-out.

SvelteKit

The SDK connects from the browser, so create stores inside onMount (guarded by the browser flag) to avoid running during server-side rendering.

svelte
<!-- src/routes/chat/+page.svelte -->
<script>
  import { browser } from '$app/environment';
  import { createChannelStore } from 'oddsockets-svelte-sdk/stores';
  import { onMount } from 'svelte';

  let store;

  onMount(() => {
    if (browser) {
      store = createChannelStore('chat', { apiKey: 'ak_your_api_key_here' });
      store.subscribe();
      return () => store.unsubscribe();
    }
  });
</script>

{#if store}
  {#each $store.messages as message}
    <div>{message.data.text}</div>
  {/each}
{:else}
  <div>Loading chat...</div>
{/if}