Skip to content

feat: realtime presence/awareness API#16

Merged
mudiageo merged 51 commits into
mainfrom
feat/realtime-presence
Jan 9, 2026
Merged

feat: realtime presence/awareness API#16
mudiageo merged 51 commits into
mainfrom
feat/realtime-presence

Conversation

@mudiageo

@mudiageo mudiageo commented Jan 1, 2026

Copy link
Copy Markdown
Owner

User description

Description

Brief description of changes.

Type of change

  • Bug fix (non-breaking change fixing an issue)
  • New feature (non-breaking change adding functionality)
  • Breaking change (fix or feature causing existing functionality to break)
  • Documentation update

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Comments added for complex code
  • Documentation updated
  • Tests added/updated
  • All tests passing
  • Changeset created (npx changeset)

Related Issues

Closes #(issue number)

Testing

How has this been tested?


PR Type

Enhancement


Description

  • Add real-time presence/awareness system for collaborative editing

  • Implement PresenceStore class tracking user status and cursor positions

  • Support user following, idle detection, and heartbeat synchronization

  • Integrate presence API into CollectionStore for easy adoption


Diagram Walkthrough

flowchart LR
  RC["RealtimeClient"] -->|"presence events"| PS["PresenceStore"]
  PS -->|"track users"| OS["Others State Map"]
  PS -->|"broadcast"| RC
  PS -->|"idle detection"| ID["Idle Timer"]
  PS -->|"heartbeat"| HB["30s Interval"]
  CS["CollectionStore"] -->|"initialize"| PS
  CS -->|"expose API"| User["User Presence API"]
Loading

File Walkthrough

Relevant files
Enhancement
presence.svelte.ts
Real-time presence store implementation                                   

src/pkg/client/presence.svelte.ts

  • New PresenceStore class managing real-time user presence and awareness
  • Tracks user status, cursor positions, selections, and editing state
  • Implements idle detection with configurable timeout and heartbeat
    synchronization
  • Provides event system for presence updates, joins, and leaves
  • Supports user following functionality and filtering by status or
    resource
+261/-0 
sync.svelte.ts
Integrate presence into CollectionStore                                   

src/pkg/client/sync.svelte.ts

  • Import PresenceStore and User type from presence module
  • Add presenceStore property to CollectionStore class
  • Add presence() method to enable presence tracking on collections
  • Method accepts user config and optional custom state
+18/-2   

@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@vercel

vercel Bot commented Jan 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
sveltekit-sync Ready Ready Preview, Comment Jan 9, 2026 8:51am

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @mudiageo, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust real-time presence and awareness system, enabling applications to track and display the online status, cursor movements, text selections, and editing activities of users within a collaborative environment. This new functionality is encapsulated in a dedicated PresenceStore class and seamlessly integrated into the existing CollectionStore, providing a straightforward way to add rich collaborative features to data-driven applications.

Highlights

  • New PresenceStore Class: Introduced a dedicated PresenceStore class to manage real-time user presence, including online status, cursor positions, text selections, and editing activities.
  • Real-time Synchronization: The PresenceStore integrates with a RealtimeClient to broadcast and receive presence updates, ensuring all connected clients have up-to-date information on other users.
  • Idle Detection and Heartbeat: Implemented automatic idle detection based on user activity (mousemove, keydown, click) and a heartbeat mechanism to periodically broadcast presence, maintaining accurate user status.
  • CollectionStore Integration: The CollectionStore now includes a new presence() method, allowing developers to easily enable and manage real-time presence and awareness for any data collection.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@qodo-code-review

qodo-code-review Bot commented Jan 1, 2026

Copy link
Copy Markdown
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Presence spoofing

Description: Incoming presence:update / presence:join / presence:leave events are trusted without any
verification (e.g., channel/table scoping, auth binding, signature, or server-issued
identity), enabling a malicious client to spoof another userId and inject arbitrary state
into othersState (impersonation / misleading presence data) if the underlying realtime
transport permits it.
presence.svelte.ts [84-107]

Referred Code
this.realtimeClient.on('presence:update', (data: any) => {
  const { userId, state } = data;
  if (userId !== this.myState.user.id) {
    this.othersState.set(userId, state);
    this.emit('update', state);
  }
});

this.realtimeClient.on('presence:join', (data: any) => {
  const { userId, state } = data;
  if (userId !== this.myState.user.id) {
    this.othersState.set(userId, state);
    this.emit('join', state);
  }
});

this.realtimeClient.on('presence:leave', (data: any) => {
  const { userId } = data;
  const state = this.othersState.get(userId);
  if (state) {
    this.othersState.delete(userId);


 ... (clipped 3 lines)
Sensitive data exposure

Description: The code broadcasts the full PresenceState (including user fields like email, plus
cursor/selection/editing details) to other clients via
realtimeClient.emit('presence:update', ...), which can unintentionally expose sensitive
personal data and behavioral/context data to unintended recipients if channel access is
broader than strictly-authorized collaborators.
presence.svelte.ts [61-148]

Referred Code
  this.myState = {
    user: { ...user, color: user.color || this.generateColor() },
    status: 'online',
    lastSeen: Date.now(),
    custom: customState
  };

  this.setupPresenceSync();
  this.startHeartbeat();
  this.setupIdleDetection();
}

private generateColor(): string {
  const colors = [
    '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', 
    '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E2'
  ];
  return colors[Math.floor(Math.random() * colors.length)];
}

private setupPresenceSync(): void {


 ... (clipped 67 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing input validation: Incoming realtime event payloads are treated as trusted (any) and directly written into
othersState without validation/guarding against malformed data, missing fields, or wrong
table/resource, risking runtime errors and inconsistent state.

Referred Code
this.realtimeClient.on('presence:update', (data: any) => {
  const { userId, state } = data;
  if (userId !== this.myState.user.id) {
    this.othersState.set(userId, state);
    this.emit('update', state);
  }
});

this.realtimeClient.on('presence:join', (data: any) => {
  const { userId, state } = data;
  if (userId !== this.myState.user.id) {
    this.othersState.set(userId, state);
    this.emit('join', state);
  }
});

this.realtimeClient.on('presence:leave', (data: any) => {
  const { userId } = data;
  const state = this.othersState.get(userId);
  if (state) {
    this.othersState.delete(userId);


 ... (clipped 3 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
No audit logging: Presence join/leave/update actions are emitted and applied without any audit logging
context (user, timestamp, outcome), which may be required if presence events are
considered security-relevant in this product.

Referred Code
this.realtimeClient.on('presence:update', (data: any) => {
  const { userId, state } = data;
  if (userId !== this.myState.user.id) {
    this.othersState.set(userId, state);
    this.emit('update', state);
  }
});

this.realtimeClient.on('presence:join', (data: any) => {
  const { userId, state } = data;
  if (userId !== this.myState.user.id) {
    this.othersState.set(userId, state);
    this.emit('join', state);
  }
});

this.realtimeClient.on('presence:leave', (data: any) => {
  const { userId } = data;
  const state = this.othersState.get(userId);
  if (state) {
    this.othersState.delete(userId);


 ... (clipped 6 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Potential PII broadcast: The full User object (including optional email) is included in myState and broadcast to
other clients, which may expose PII unless the system explicitly restricts presence fields
to non-sensitive data.

Referred Code
export interface User {
  id: string;
  name: string;
  email?: string;
  avatar?: string;
  color?: string;
}

export interface CursorPosition {
  x: number;
  y: number;
  line?: number;
  column?: number;
}

export interface Selection {
  start: CursorPosition;
  end: CursorPosition;
  text?: string;
}



 ... (clipped 42 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@mudiageo mudiageo changed the title initial implementation feat: realtime presence/awareness API Jan 1, 2026
@qodo-code-review

qodo-code-review Bot commented Jan 1, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Implement the missing server-side presence logic

The PR introduces a client-side presence system but omits the necessary
server-side implementation. A server component is needed to receive, manage, and
broadcast presence events to make the feature functional.

Examples:

src/pkg/client/presence.svelte.ts [81-111]
  private setupPresenceSync(): void {
    if (!this.realtimeClient) return;

    this.realtimeClient.on('presence:update', (data: any) => {
      const { userId, state } = data;
      if (userId !== this.myState.user.id) {
        this.othersState.set(userId, state);
        this.emit('update', state);
      }
    });

 ... (clipped 21 lines)

Solution Walkthrough:

Before:

// src/pkg/client/presence.svelte.ts
class PresenceStore {
  private realtimeClient: RealtimeClient;
  // ...
  private broadcastPresence(): void {
    // Emits an event to a server that is not implemented.
    this.realtimeClient.emit('presence:update', {
      table: this.tableName,
      userId: this.myState.user.id,
      state: this.myState
    });
  }

  private setupPresenceSync(): void {
    // Listens for events from other users, but no server
    // is implemented to broadcast them.
    this.realtimeClient.on('presence:update', (data: any) => { ... });
    this.realtimeClient.on('presence:join', (data: any) => { ... });
    this.realtimeClient.on('presence:leave', (data: any) => { ... });
  }
}

After:

// Conceptual server-side implementation
// e.g., server/presence-handler.ts

class PresenceServer {
  // ...
  handleConnection(socket) {
    socket.on('presence:update', (data) => {
      // Logic to store presence state for the user.
      // ...

      // Broadcast the update to other clients in the same room/table.
      socket.to(data.table).emit('presence:update', {
        userId: data.userId,
        state: data.state
      });
    });

    // ... handle 'presence:leave', disconnects, etc.
  }
}
Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies a critical architectural gap; the client-side presence feature is incomplete and non-functional without the corresponding server-side logic to manage and relay events.

High
Possible issue
Preserve reactive state reference

Use Object.assign(this.myState, ...) instead of this.myState = ... to update the
reactive $state object in the constructor, preserving its reactivity.

src/pkg/client/presence.svelte.ts [61-66]

-this.myState = {
+Object.assign(this.myState, {
   user: { ...user, color: user.color || this.generateColor() },
   status: 'online',
   lastSeen: Date.now(),
   custom: customState
-};
+});
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies a critical issue with Svelte 5 reactivity. Reassigning a $state variable breaks its connection, and using Object.assign is the correct way to update it while preserving reactivity.

High
Prevent memory leaks from idle detection
Suggestion Impact:The commit moved the inline resetIdle arrow function into a class method (resetIdle), updated addEventListener calls to use that method reference, and added matching removeEventListener calls in destroy(), addressing the potential memory leak.

code diff:

   private setupIdleDetection(): void {
-    const resetIdle = () => {
-      if (this.myState.status === 'idle') {
-        this.setActive();
-      }
-      this.resetIdleTimer();
-    };
-
-    window.addEventListener('mousemove', resetIdle);
-    window.addEventListener('keydown', resetIdle);
-    window.addEventListener('click', resetIdle);
-
+    window.addEventListener('mousemove', this.resetIdle);
+    window.addEventListener('keydown', this.resetIdle);
+    window.addEventListener('click', this.resetIdle);
+
+    this.resetIdleTimer();
+  }
+
+  private resetIdle(): void {
+    if (this.myState.status === 'idle') {
+      this.setActive();
+    }
     this.resetIdleTimer();
   }
 
@@ -141,10 +143,12 @@
     if (!this.realtimeClient) return;
     
     this.myState.lastSeen = Date.now();
-    this.realtimeClient.emit('presence:update', {
-      table: this.tableName,
-      userId: this.myState.user.id,
+    // Use send() instead of emit() for client-to-server communication
+    this.realtimeClient.send('presence:update', {
+      channel: this.tableName,
       state: this.myState
+    })?.catch((error: unknown) => {
+      console.error('Failed to broadcast presence:', error);
     });
   }
 
@@ -178,7 +182,14 @@
 
   updateCursor(position: CursorPosition): void {
     this.myState.cursor = position;
-    this.broadcastPresence();
+    
+    // Debounce cursor updates to avoid flooding the server
+    if (this.cursorDebounceTimer) {
+      clearTimeout(this.cursorDebounceTimer);
+    }
+    this.cursorDebounceTimer = window.setTimeout(() => {
+      this.broadcastPresence();
+    }, this.cursorDebounceMs);
   }
 
   updateSelection(selection: Selection | null): void {
@@ -191,12 +202,20 @@
     this.broadcastPresence();
   }
 
+  get myPresence(): PresenceState<T> {
+    return this.myState;
+  }
+
   get others(): PresenceState<T>[] {
     return Array.from(this.othersState.values());
   }
 
   get othersCount(): number {
     return this.othersState.size;
+  }
+
+  get onlineCount(): number {
+    return this.others.reduce((acc, u) => u.status === 'online' ? acc + 1 : acc, this.myState.status === 'online' ? 1 : 0);
   }
 
   getUser(userId: string): PresenceState<T> | null {
@@ -248,11 +267,19 @@
   destroy(): void {
     if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
     if (this.idleTimer) clearTimeout(this.idleTimer);
+    if (this.cursorDebounceTimer) clearTimeout(this.cursorDebounceTimer);
+
+    window.removeEventListener('mousemove', this.resetIdle);
+    window.removeEventListener('keydown', this.resetIdle);
+    window.removeEventListener('click', this.resetIdle);
     

To prevent a memory leak, refactor the resetIdle arrow function in
setupIdleDetection into a class method so it can be referenced and removed in
the destroy method.

src/pkg/client/presence.svelte.ts [118-131]

 private setupIdleDetection(): void {
-  const resetIdle = () => {
-    if (this.myState.status === 'idle') {
-      this.setActive();
-    }
-    this.resetIdleTimer();
-  };
-
-  window.addEventListener('mousemove', resetIdle);
-  window.addEventListener('keydown', resetIdle);
-  window.addEventListener('click', resetIdle);
+  window.addEventListener('mousemove', this.idleResetHandler);
+  window.addEventListener('keydown', this.idleResetHandler);
+  window.addEventListener('click', this.idleResetHandler);
 
   this.resetIdleTimer();
 }
 
+private resetIdle(): void {
+  if (this.myState.status === 'idle') {
+    this.setActive();
+  }
+  this.resetIdleTimer();
+}
+

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a memory leak caused by not removing window event listeners and proposes a valid solution, which is critical for preventing resource exhaustion in a long-running application.

Medium
  • Update

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a real-time presence system, which is a great new feature. The implementation in presence.svelte.ts is comprehensive, covering user state, idle detection, and a heartbeat mechanism. The integration into CollectionStore is clean and provides a simple API for enabling presence on a collection.

My review focuses on improving type safety and fixing a potential memory leak. Specifically, I've pointed out:

  • A memory leak in presence.svelte.ts due to un-removed global event listeners.
  • Several opportunities to replace any with stronger types, particularly in the event handling systems, to make the code more robust.
  • A type mismatch in sync.svelte.ts regarding the presenceStore property.
  • The use of hardcoded magic numbers for timers, which could be extracted into constants for better readability.

Overall, this is a solid initial implementation. Addressing these points will enhance the code's reliability and maintainability.

Comment on lines +118 to +131
private setupIdleDetection(): void {
const resetIdle = () => {
if (this.myState.status === 'idle') {
this.setActive();
}
this.resetIdleTimer();
};

window.addEventListener('mousemove', resetIdle);
window.addEventListener('keydown', resetIdle);
window.addEventListener('click', resetIdle);

this.resetIdleTimer();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The setupIdleDetection method adds global event listeners to window, but they are never removed in the destroy method. This creates a memory leak, as each PresenceStore instance will leave dangling listeners that can cause unexpected behavior even after the store is destroyed.

To fix this, you should promote the resetIdle function to a class method and then remove the listeners in destroy().

Here's how you can refactor:

  1. Move resetIdle out of setupIdleDetection and make it a private class method:

    private resetIdle = (): void => {
      if (this.myState.status === 'idle') {
        this.setActive();
      }
      this.resetIdleTimer();
    };
  2. Update setupIdleDetection to use the new class method:

    private setupIdleDetection(): void {
      window.addEventListener('mousemove', this.resetIdle);
      window.addEventListener('keydown', this.resetIdle);
      window.addEventListener('click', this.resetIdle);
    
      this.resetIdleTimer();
    }
  3. Update destroy() to remove the listeners:

    destroy(): void {
      if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
      if (this.idleTimer) clearTimeout(this.idleTimer);
    
      window.removeEventListener('mousemove', this.resetIdle);
      window.removeEventListener('keydown', this.resetIdle);
      window.removeEventListener('click', this.resetIdle);
      
      if (this.realtimeClient) {
        // ...
      }
      
      this.eventListeners.clear();
    }


private _fields: FieldsProxy<T>;

private presenceStore: PresenceStore<T> | null = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The presenceStore property is typed as PresenceStore<T>, where T is the type of the collection's documents. However, the presence() method initializes it with a custom state of type any, creating a PresenceStore<any>. This assignment is not type-safe because the custom presence data is unrelated to the collection's data type T.

To fix this, the property should be typed as PresenceStore<any> | null.

For even better type safety, you could make the presence() method generic to capture the type of the custom state:

presence<CustomData = any>(config: { user: User; custom?: CustomData }): PresenceStore<CustomData> {
  if (!this.presenceStore) {
    this.presenceStore = new PresenceStore(
      this.engine.realtime,
      this.tableName,
      config.user,
      config.custom
    );
  }
  return this.presenceStore as PresenceStore<CustomData>;
}
Suggested change
private presenceStore: PresenceStore<T> | null = null;
private presenceStore: PresenceStore<any> | null = null;

private followingUserId: string | null = $state(null);
private heartbeatInterval: number | null = null;
private idleTimer: number | null = null;
private eventListeners: Map<string, Set<(data: any) => void>> = new Map();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The eventListeners map uses (data: any) => void for its handlers, which weakens type safety. You can create a much safer event system by defining an interface for your events and using generic types to provide strong typing for event payloads.

For example:

interface PresenceEvents<T> {
  'update': PresenceState<T>;
  'join': PresenceState<T>;
  'leave': PresenceState<T>;
  'following:update': PresenceState<T>;
}

Then, you can update your on and emit methods to be strongly typed:

// Update 'on' method signature
on<K extends keyof PresenceEvents<T>>(event: K, handler: (data: PresenceEvents<T>[K]) => void): () => void

// Update 'emit' method signature
private emit<K extends keyof PresenceEvents<T>>(event: K, data: PresenceEvents<T>[K]): void

This will provide type checking and autocompletion for event names and their payloads, preventing common bugs.

Comment thread src/pkg/client/presence.svelte.ts Outdated
Comment on lines +113 to +115
this.heartbeatInterval = window.setInterval(() => {
this.broadcastPresence();
}, 30000);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The heartbeat interval is hardcoded to 30000. Using magic numbers like this can make the code difficult to understand and maintain. It's better to extract this value into a named constant at the top of the file, for example:

const HEARTBEAT_INTERVAL_MS = 30000;

This same principle applies to the idle timeout (5 * 60 * 1000) in the resetIdleTimer method. For greater flexibility, you could also consider making these values configurable through the PresenceStore constructor.

Copilot AI and others added 19 commits January 7, 2026 20:44
- Created EphemeralStore for in-memory presence/ephemeral data
- Added presence event types to realtime types
- Updated RealtimeServer with presence and ephemeral handlers
- Added POST endpoint in ServerSyncEngine for client->server messages
- Added send() method to RealtimeClient for bidirectional communication
- Updated PresenceStore to use send() instead of emit()
- Added debouncing for cursor updates (50ms)

Co-authored-by: mudiageo <76590594+mudiageo@users.noreply.github.com>
- Created ephemeral-store.test.ts with 25 test cases
- Fixed import path in presence.svelte.ts
- Fixed error type annotations to use 'unknown'
- All tests passing successfully

Co-authored-by: mudiageo <76590594+mudiageo@users.noreply.github.com>
- Created SyncChannel for channel-scoped real-time communication
- Added channel() method to SyncEngine
- Implemented presence tracking, custom events, subscribe/unsubscribe
- Created comprehensive channel tests (browser tests need Playwright)

Co-authored-by: mudiageo <76590594+mudiageo@users.noreply.github.com>
- Updated src/pkg/index.ts to export SyncChannel, PresenceStore, RealtimeClient
- Exported presence and realtime types (User, CursorPosition, PresenceState, etc.)
- Updated src/pkg/server/index.ts to export RealtimeServer and EphemeralStore
- Exported server realtime types

Co-authored-by: mudiageo <76590594+mudiageo@users.noreply.github.com>
Co-authored-by: qodo-code-review[bot] <151058649+qodo-code-review[bot]@users.noreply.github.com>
Co-authored-by: qodo-code-review[bot] <151058649+qodo-code-review[bot]@users.noreply.github.com>
Co-authored-by: qodo-code-review[bot] <151058649+qodo-code-review[bot]@users.noreply.github.com>
Co-authored-by: qodo-code-review[bot] <151058649+qodo-code-review[bot]@users.noreply.github.com>
Co-authored-by: qodo-code-review[bot] <151058649+qodo-code-review[bot]@users.noreply.github.com>
- Add 9 comprehensive multi-user/multi-client scenario tests
- Create presence/awareness documentation (content/docs/presence-awareness.md)
- Add internal architecture document (docs/REALTIME_ARCHITECTURE.md)
- Create changesets for feature (minor) and API change (patch)
- Enhance todos route with real-time collaborator avatars and status
- Enhance notes route with editing indicators and presence
- All tests passing (82 total: 73 existing + 9 new)

Co-authored-by: mudiageo <76590594+mudiageo@users.noreply.github.com>
… store

Co-authored-by: mudiageo <76590594+mudiageo@users.noreply.github.com>
- Add 3 integration tests for collection.presence() API
- Update notes route to use collection.presence() directly (simpler than channel)
- Restructure docs: collection.presence() as primary, channels as alternative
- Clarify handle vs explicit routes (use one, not both)
- Document when to use each API approach

Co-authored-by: mudiageo <76590594+mudiageo@users.noreply.github.com>
… store

Co-authored-by: mudiageo <76590594+mudiageo@users.noreply.github.com>
- Created usePresence hook for simplified Svelte 5 DX
- Implemented awareness utilities: useCursorTracking, useSelectionTracking, useWhoIsHere
- Added comprehensive tests for all new functionality (16 new tests)
- Exported new hooks and utilities from main index
- All utilities use Svelte 5 runes for reactivity

Co-authored-by: mudiageo <76590594+mudiageo@users.noreply.github.com>
- Updated presence-awareness.md with hooks & utilities section
- Added detailed examples for usePresence, useCursorTracking, useSelectionTracking, useWhoIsHere
- Created HOOKS_USAGE_EXAMPLE.md with complete collaborative whiteboard example
- Included API comparisons and real-world patterns

Co-authored-by: mudiageo <76590594+mudiageo@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Comment thread src/pkg/client/presence.svelte.ts Outdated
Comment on lines +118 to +130
}

private setupIdleDetection(): void {
const resetIdle = () => {
if (this.myState.status === 'idle') {
this.setActive();
}
this.resetIdleTimer();
};

window.addEventListener('mousemove', resetIdle);
window.addEventListener('keydown', resetIdle);
window.addEventListener('click', resetIdle);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Prevent memory leaks from idle detection

Suggested change
}
private setupIdleDetection(): void {
const resetIdle = () => {
if (this.myState.status === 'idle') {
this.setActive();
}
this.resetIdleTimer();
};
window.addEventListener('mousemove', resetIdle);
window.addEventListener('keydown', resetIdle);
window.addEventListener('click', resetIdle);
private setupIdleDetection(): void {
window.addEventListener('mousemove', this.idleResetHandler);
window.addEventListener('keydown', this.idleResetHandler);
window.addEventListener('click', this.idleResetHandler);
this.resetIdleTimer();
}
private resetIdle(): void {
if (this.myState.status === 'idle') {
this.setActive();
}
this.resetIdleTimer();

@mudiageo
mudiageo merged commit 9ce2d11 into main Jan 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants