OddSockets Svelte SDK
Official Svelte SDK with reactive stores and components for OddSockets real-time messaging
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
npm install oddsockets-svelte
yarn add oddsockets-svelte
pnpm add oddsockets-svelte
Quick Start
Basic Chat with Reactive Stores
<script>
import { createChannelStore } from 'oddsockets-svelte/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
<script>
import { createConnectionStore } from 'oddsockets-svelte/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
<script>
import { createChannelStore } from 'oddsockets-svelte/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
<script>
import { createMultiChannelStore } from 'oddsockets-svelte/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
import { createOddSocketsClient } from 'oddsockets-svelte';
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
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(...):
- Typing —
startTyping,stopTyping→user_typing,user_stopped_typing - Reactions —
addReaction,removeReaction,getReactions→reaction_added,reaction_removed - Threads —
threadReply,getThread,subscribeThread,followThread,markThreadRead→thread_reply,thread_subscribed,thread_followed,thread_read_updated - Read receipts —
markRead,markAllRead,getUnreadCounts→user_read,unread_count_updated,all_marked_read - Messages —
editMessage,deleteMessage,pinMessage,unpinMessage,getPinnedMessages→message_edited,message_deleted,message_pinned,message_unpinned - Presence & status —
setStatus,setCustomStatus,setDND,getUserPresence→user_status_changed,custom_status_updated,dnd_status_changed - Channels —
createChannel,updateChannel,archiveChannel,inviteToChannel,joinChannel,leaveChannel→channel_created,channel_updated,user_invited,user_joined_channel,user_left_channel - DMs —
createDM,sendDM,getDMConversations→dm_created,dm_received - Notifications —
subscribeNotifications,getNotifications,markNotificationRead,clearNotifications→notification,notification_read,notifications_cleared - Search —
searchMessages,searchInChannel,searchByUser,filterMessages(results returned viaPromise)
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).
// 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 → ackchallenge_create_successreportProgress— fire-and-forget metric progress (no ack)completeChallenge(await) — finalize with an outcome → ackchallenge_complete_successunlockAchievement— fire-and-forget; passpercentComplete(0–100) (no ack)getStandings(await) — request top-N + caller rank → ackchallenge_standings_successgetAchievements(await) — query achievement state → ackachievement_statesendChallengeInvite(await) — directed invite to another user → ackchallenge_invite_successreplyChallengeInvite(await) — accept/decline an invite → ackchallenge_reply_successcancelChallengeInvite(await) — cancel a sent invite → ackchallenge_invite_cancel_successgetChallengeInvites(await) — list pending invites → ackchallenge_invites
Outcome vocabulary
The outcome passed to completeChallenge is one of:
completed— win (rank 1)failed— losstied— drawconceded— resign / concedeexpired— 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 broadcasts —
challenge_progress,leaderboard_rank_change,challenge_complete,achievement_unlock,achievement_progress - Directed (per-user) —
challenge_invited,challenge_reply_received,challenge_invite_cancelled
Usage Analytics
Pull your tenant's headline usage tiles — monthly active users, daily active users, total messages published, and error rate — straight from the SDK, without hand-rolling a REST call. getUsageStats() resolves the same four tiles the developer dashboard renders.
const stats = await client.getUsageStats();
// {
// mau: 1240, // monthly active users, or null
// dau: 210, // daily active users, or null
// totalMessages: 84213, // messages published, or null
// errorRate: 0.002, // 0-1, or null
// ownerScope: 'ak_...owner',
// detail: null,
// timestamp: '2026-09-04T12:00:00.000Z'
// }
console.log(`MAU: ${stats.mau ?? '\u2014'}`); // render an em-dash when null
The four tiles
- mau — monthly active users for your owner scope
- dau — daily active users
- totalMessages — total messages published
- errorRate — publish error rate,
0–1
Honesty rule — null, never a fake zero
Each tile is a number or null. A null means that leg of the analytics pipeline is not live yet for your tenant — the SDK returns null verbatim and never coerces it to 0. Render an em-dash (—) for a null tile so you never show a fabricated zero.
Requires an API key
getUsageStats() reads your owner-scoped analytics, so it needs an apiKey. Keyless / token-only clients have no owner scope to query and will throw getUsageStats requires an apiKey.
Performance
Keep reactive rendering fast by capping the messages you render and keying each loop.
Efficient Rendering
<script>
import { createChannelStore } from 'oddsockets-svelte/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.
<!-- src/routes/chat/+page.svelte -->
<script>
import { browser } from '$app/environment';
import { createChannelStore } from 'oddsockets-svelte/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}