sveltekit-sync
Version:
Local-first sync engine for SvelteKit
140 lines (139 loc) • 4.13 kB
JavaScript
/**
* SyncChannel - Channel-based API for real-time features
*
* Provides a composable API for presence tracking, custom events,
* and channel-scoped communication.
*/
import { PresenceStore } from './presence.svelte.js';
/**
* Channel for scoped real-time communication
*/
export class SyncChannel {
name;
presence = null;
realtimeClient;
options;
subscribed = false;
eventHandlers = new Map();
constructor(realtimeClient, name, user, options = {}) {
this.realtimeClient = realtimeClient;
this.name = name;
this.options = {
presence: options.presence ?? false,
broadcast: options.broadcast ?? false,
};
// Initialize presence if enabled
if (this.options.presence && user) {
this.presence = new PresenceStore(realtimeClient, name, user);
}
// Setup event listeners for channel-specific events
this.setupEventListeners();
}
unsubscribeEphemeral;
setupEventListeners() {
// Listen for ephemeral events on this channel
this.unsubscribeEphemeral = this.realtimeClient.on('ephemeral', (data) => {
if (data.channel === this.name) {
const handlers = this.eventHandlers.get(data.event);
if (handlers) {
handlers.forEach(handler => handler(data.data));
}
}
});
}
/**
* Subscribe to the channel
*/
async subscribe() {
if (this.subscribed) {
console.warn("Already subscribed to channel");
return;
}
await this.realtimeClient.joinChannel(this.name);
this.subscribed = true;
}
async unsubscribe() {
if (!this.subscribed) {
return;
}
await this.realtimeClient.leaveChannel(this.name);
this.subscribed = false;
// Clean up presence if enabled
if (this.presence) {
this.presence.destroy();
}
// Remove ephemeral listener
if (this.unsubscribeEphemeral) {
this.unsubscribeEphemeral();
this.unsubscribeEphemeral = undefined;
}
// Clear event handlers
this.eventHandlers.clear();
}
/**
* Track presence state (if presence is enabled)
*/
track(state) {
if (!this.presence) {
console.warn('Presence is not enabled for this channel');
return;
}
this.presence.updatePresence({ custom: state });
}
/**
* Stop tracking presence
*/
untrack() {
if (!this.presence) {
return;
}
this.presence.setStatus('offline');
}
/**
* Listen for custom events on this channel
*/
on(event, handler) {
if (!this.options.broadcast) {
console.warn('Broadcasting is not enabled for this channel');
return () => { };
}
if (!this.eventHandlers.has(event)) {
this.eventHandlers.set(event, new Set());
}
this.eventHandlers.get(event).add(handler);
// Return unsubscribe function
return () => {
const handlers = this.eventHandlers.get(event);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
this.eventHandlers.delete(event);
}
}
};
}
/**
* Broadcast a custom event to all subscribers of this channel
*/
async broadcast(event, data) {
if (!this.options.broadcast) {
console.warn('Broadcasting is not enabled for this channel');
return;
}
if (!this.subscribed) {
console.warn(`Cannot broadcast on unsubscribed channel ${this.name}`);
return;
}
await this.realtimeClient.send('ephemeral', {
channel: this.name,
event,
data
});
}
/**
* Check if the channel is subscribed
*/
isSubscribed() {
return this.subscribed;
}
}