sveltekit-sync
Version:
Local-first sync engine for SvelteKit
138 lines (137 loc) • 4.34 kB
JavaScript
/**
* usePresence Hook
*
* Svelte 5 runes-compatible hook for simplified presence/awareness usage.
* Provides reactive state, automatic tracking, and event handling.
*/
import { PresenceStore } from '../presence.svelte.js';
/**
* Create a presence hook for a channel
*
* @param channel - The SyncChannel to use for presence
* @param user - Current user information
* @param options - Configuration options
* @returns Presence API with reactive state and actions
*
* @example
* ```typescript
* const presence = usePresence(channel, currentUser, {
* customState: { editing: 'doc-123' },
* trackCursor: true,
* idleTimeout: 300000 // 5 minutes
* });
*
* // Access reactive state
* const collaborators = presence.others;
* const count = presence.onlineCount;
*
* // Update presence
* presence.updateCursor({ x: 100, y: 200 });
* presence.updateCustom({ editing: 'section-2' });
* ```
*/
export function usePresence(channel, user, options) {
const opts = {
idleTimeout: 300000, // 5 minutes
trackCursor: false,
trackSelection: false,
...options
};
// Get or create presence store from channel
const presence = channel.presence;
if (!presence) {
throw new Error('Channel must have presence enabled');
}
// Setup automatic cursor tracking if enabled
let cursorTrackingCleanup = null;
if (opts.trackCursor && typeof document !== 'undefined') {
const handleMouseMove = (e) => {
presence.updateCursor({
x: e.clientX,
y: e.clientY
});
};
document.addEventListener('mousemove', handleMouseMove);
cursorTrackingCleanup = () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}
// Setup automatic selection tracking if enabled
let selectionTrackingCleanup = null;
if (opts.trackSelection && typeof document !== 'undefined') {
const handleSelectionChange = () => {
const selection = document.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
presence.updateSelection({
start: { x: rect.left, y: rect.top },
end: { x: rect.right, y: rect.bottom },
text: selection.toString()
});
}
else {
presence.updateSelection(null);
}
};
document.addEventListener('selectionchange', handleSelectionChange);
selectionTrackingCleanup = () => {
document.removeEventListener('selectionchange', handleSelectionChange);
};
}
return {
// Reactive state
get others() {
return presence.others;
},
get othersCount() {
return presence.othersCount;
},
get onlineCount() {
return presence.onlineCount;
},
get myPresence() {
return presence.myPresence;
},
// Actions
updateCursor(position) {
presence.updateCursor(position);
},
updateSelection(selection) {
presence.updateSelection(selection);
},
updateEditing(editing) {
presence.updateEditing(editing);
},
updateCustom(custom) {
presence.updatePresence({ custom });
},
setStatus(status) {
presence.setStatus(status);
},
// Queries
getUser(userId) {
return presence.getUser(userId);
},
getUsersEditing(resourceId) {
return presence.getUsersEditing(resourceId);
},
// Follow mode
follow(userId) {
return presence.follow(userId);
},
isFollowing(userId) {
return presence.isFollowing(userId);
},
// Events
on(event, handler) {
return presence.on(event, handler);
},
// Cleanup
destroy() {
cursorTrackingCleanup?.();
selectionTrackingCleanup?.();
// Note: Don't destroy the underlying presence store as it's managed by the channel
}
};
}