or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

events.mdindex.mdlegacy-factories.mdshared-directory.mdshared-map.md
tile.json

events.mddocs/

Event System

The @fluidframework/map package provides a comprehensive event system for both SharedMap and SharedDirectory, enabling reactive programming patterns and real-time collaborative features. All events are emitted when changes occur, whether from the local client or remote clients.

Capabilities

Base Event Interfaces

Core event interfaces that define the structure of event data passed to listeners.

/**
 * Base interface for value change events
 */
interface IValueChanged {
  /** The key storing the value that changed */
  readonly key: string;
  /** The value that was stored at the key prior to the change */
  readonly previousValue: any;
}

/**
 * Extended value change event for directories with path information
 */
interface IDirectoryValueChanged extends IValueChanged {
  /** The absolute path to the IDirectory storing the key which changed */
  path: string;
}

/**
 * Base event provider interface
 */
interface IEventProvider<TEvent extends IEvent> {
  on<K>(event: K, listener: TEvent[K]): this;
  once<K>(event: K, listener: TEvent[K]): this;
  off<K>(event: K, listener: TEvent[K]): this;
}

/**
 * Placeholder interface for event listener 'this' context
 */
interface IEventThisPlaceHolder {
  // Context placeholder
}

SharedMap Events

SharedMap emits events when its key-value data changes, providing information about what changed and whether the change was local or remote.

/**
 * Events emitted by SharedMap instances
 */
interface ISharedMapEvents extends ISharedObjectEvents {
  /**
   * Emitted when a key is set or deleted
   * @param changed - Information on the key that changed and its value prior to the change
   * @param local - Whether the change originated from this client
   * @param target - The ISharedMap itself
   */
  (event: "valueChanged", listener: (changed: IValueChanged, local: boolean, target: IEventThisPlaceHolder) => void): any;

  /**
   * Emitted when the map is cleared
   * @param local - Whether the clear originated from this client
   * @param target - The ISharedMap itself
   */
  (event: "clear", listener: (local: boolean, target: IEventThisPlaceHolder) => void): any;
}

Usage Examples:

import { SharedMap } from "@fluidframework/map";

const myMap = SharedMap.create(runtime, "example-map");

// Listen for value changes
myMap.on("valueChanged", (changed, local, target) => {
  console.log(`Key "${changed.key}" changed`);
  console.log(`Previous value:`, changed.previousValue);
  console.log(`New value:`, myMap.get(changed.key));
  console.log(`Change origin: ${local ? "local" : "remote"}`);
  console.log(`Target map size:`, target.size);
});

// Listen for clear events
myMap.on("clear", (local, target) => {
  console.log(`Map cleared ${local ? "locally" : "by remote client"}`);
  console.log(`Map is now empty: ${target.size === 0}`);
});

// Trigger events
myMap.set("username", "alice"); // Emits valueChanged
myMap.set("username", "bob");   // Emits valueChanged (previous: "alice")
myMap.delete("username");       // Emits valueChanged (previous: "bob")
myMap.clear();                  // Emits clear

SharedDirectory Events

SharedDirectory emits two types of events: SharedDirectory-level events that capture changes anywhere in the hierarchy, and Directory-level events that capture changes specific to individual directories.

SharedDirectory-Level Events

These events are emitted on the root SharedDirectory instance and capture all changes throughout the entire directory hierarchy.

/**
 * Events emitted by SharedDirectory instances for hierarchy-wide changes
 */
interface ISharedDirectoryEvents extends ISharedObjectEvents {
  /**
   * Emitted when a key is set or deleted anywhere in the directory hierarchy
   * @param changed - Information on the key that changed, its previous value, and the path
   * @param local - Whether the change originated from this client
   * @param target - The ISharedDirectory itself
   */
  (event: "valueChanged", listener: (changed: IDirectoryValueChanged, local: boolean, target: IEventThisPlaceHolder) => void): any;

  /**
   * Emitted when any directory in the hierarchy is cleared
   * @param local - Whether the clear originated from this client
   * @param target - The ISharedDirectory itself
   */
  (event: "clear", listener: (local: boolean, target: IEventThisPlaceHolder) => void): any;

  /**
   * Emitted when a subdirectory is created anywhere in the hierarchy
   * @param path - The relative path to the subdirectory that was created
   * @param local - Whether the create originated from this client
   * @param target - The ISharedDirectory itself
   */
  (event: "subDirectoryCreated", listener: (path: string, local: boolean, target: IEventThisPlaceHolder) => void): any;

  /**
   * Emitted when a subdirectory is deleted anywhere in the hierarchy
   * @param path - The relative path to the subdirectory that was deleted
   * @param local - Whether the delete originated from this client
   * @param target - The ISharedDirectory itself
   */
  (event: "subDirectoryDeleted", listener: (path: string, local: boolean, target: IEventThisPlaceHolder) => void): any;
}

Usage Examples:

import { SharedDirectory } from "@fluidframework/map";

const appData = SharedDirectory.create(runtime, "app");

// Monitor all value changes throughout the hierarchy
appData.on("valueChanged", (changed, local, target) => {
  console.log(`Value changed at ${changed.path}/${changed.key}`);
  console.log(`Previous:`, changed.previousValue);
  console.log(`Current:`, appData.getWorkingDirectory(changed.path)?.get(changed.key));
  console.log(`Origin: ${local ? "local" : "remote"}`);
});

// Monitor directory structure changes
appData.on("subDirectoryCreated", (path, local, target) => {
  console.log(`Directory created: ${path} (${local ? "local" : "remote"})`);
  const newDir = appData.getWorkingDirectory(path);
  if (newDir) {
    console.log(`New directory absolute path: ${newDir.absolutePath}`);
  }
});

appData.on("subDirectoryDeleted", (path, local, target) => {
  console.log(`Directory deleted: ${path} (${local ? "local" : "remote"})`);
});

appData.on("clear", (local, target) => {
  console.log(`Directory cleared ${local ? "locally" : "remotely"}`);
});

// Test the events
const users = appData.createSubDirectory("users");        // Emits subDirectoryCreated
const alice = users.createSubDirectory("alice");          // Emits subDirectoryCreated
alice.set("email", "alice@example.com");                  // Emits valueChanged
alice.set("role", "admin");                               // Emits valueChanged
users.deleteSubDirectory("alice");                        // Emits subDirectoryDeleted
appData.clear();                                          // Emits clear

Directory-Level Events

These events are emitted on individual directory instances and only capture changes that directly affect that specific directory.

/**
 * Events emitted by individual IDirectory instances
 */
interface IDirectoryEvents extends IEvent {
  /**
   * Emitted when a key is set or deleted directly in this directory
   * @param changed - Information on the key that changed and its value prior to the change
   * @param local - Whether the change originated from this client
   * @param target - The IDirectory itself
   */
  (event: "containedValueChanged", listener: (changed: IValueChanged, local: boolean, target: IEventThisPlaceHolder) => void): any;

  /**
   * Emitted when a direct subdirectory is created
   * @param path - The relative path to the subdirectory that was created
   * @param local - Whether the creation originated from this client
   * @param target - The IDirectory itself
   */
  (event: "subDirectoryCreated", listener: (path: string, local: boolean, target: IEventThisPlaceHolder) => void): any;

  /**
   * Emitted when a direct subdirectory is deleted
   * @param path - The relative path to the subdirectory that was deleted
   * @param local - Whether the delete originated from this client
   * @param target - The IDirectory itself
   */
  (event: "subDirectoryDeleted", listener: (path: string, local: boolean, target: IEventThisPlaceHolder) => void): any;

  /**
   * Emitted when this directory is deleted
   * @param target - The IDirectory itself
   */
  (event: "disposed", listener: (target: IEventThisPlaceHolder) => void): any;

  /**
   * Emitted when this previously deleted directory is restored (rollback scenarios)
   * @param target - The IDirectory itself
   */
  (event: "undisposed", listener: (target: IEventThisPlaceHolder) => void): any;
}

Usage Examples:

// Create directory structure
const appData = SharedDirectory.create(runtime, "app");
const users = appData.createSubDirectory("users");
const settings = appData.createSubDirectory("settings");

// Listen for changes directly in the users directory
users.on("containedValueChanged", (changed, local, target) => {
  console.log(`Direct change in users directory: ${changed.key}`);
  console.log(`Previous value:`, changed.previousValue);
  console.log(`New value:`, users.get(changed.key));
});

// Listen for subdirectory creation in users directory
users.on("subDirectoryCreated", (path, local, target) => {
  console.log(`New user directory created: ${path}`);
});

// Listen for subdirectory deletion in users directory
users.on("subDirectoryDeleted", (path, local, target) => {
  console.log(`User directory deleted: ${path}`);
});

// Listen for the users directory being deleted
users.on("disposed", (target) => {
  console.log("Users directory has been disposed");
  // Any further operations on this directory will throw errors
});

// Listen for the users directory being restored (rollback)
users.on("undisposed", (target) => {
  console.log("Users directory has been restored");
});

// Test the events
users.set("total-count", 0);                    // Emits containedValueChanged
const alice = users.createSubDirectory("alice"); // Emits subDirectoryCreated
alice.set("email", "alice@example.com");        // Does NOT emit containedValueChanged on users
users.deleteSubDirectory("alice");              // Emits subDirectoryDeleted
appData.deleteSubDirectory("users");            // Emits disposed on users directory

Shared Object Base Events

Both SharedMap and SharedDirectory inherit from the base shared object infrastructure, providing connection and lifecycle events.

/**
 * Base events for all shared objects
 */
interface ISharedObjectEvents extends IEvent {
  /**
   * Emitted when the shared object connects to or disconnects from the Fluid service
   * @param value - True when connected, false when disconnected
   * @param clientId - ID of the client that connected/disconnected
   */
  (event: "connected", listener: (value: boolean, clientId: string) => void): any;

  /**
   * Emitted when the shared object is disconnected from the Fluid service
   */
  (event: "disconnected", listener: () => void): any;

  /**
   * Emitted when the shared object is disposed and no longer usable
   */
  (event: "disposed", listener: () => void): any;
}

Usage Examples:

const myMap = SharedMap.create(runtime, "connection-aware-map");

// Listen for connection state changes
myMap.on("connected", (isConnected, clientId) => {
  if (isConnected) {
    console.log(`Connected to Fluid service (client: ${clientId})`);
    // Safe to perform operations that require connectivity
  } else {
    console.log(`Disconnected from Fluid service (client: ${clientId})`);
    // Operations will be queued until reconnection
  }
});

myMap.on("disconnected", () => {
  console.log("Lost connection to Fluid service");
  // Show offline indicator to user
});

myMap.on("disposed", () => {
  console.log("Shared object has been disposed");
  // Clean up any references and event listeners
});

// Check current connection state
console.log(`Currently connected: ${myMap.connected}`);

Event Management

All Fluid Framework objects provide standard event management methods for adding, removing, and managing event listeners.

/**
 * Add an event listener
 * @param event - Event name to listen for
 * @param listener - Function to call when event is emitted
 * @returns The object itself for chaining
 */
on<K>(event: K, listener: EventListener<K>): this;

/**
 * Add a one-time event listener that automatically removes itself after first emission
 * @param event - Event name to listen for
 * @param listener - Function to call when event is emitted
 * @returns The object itself for chaining
 */
once<K>(event: K, listener: EventListener<K>): this;

/**
 * Remove an event listener
 * @param event - Event name to stop listening for
 * @param listener - The specific listener function to remove
 * @returns The object itself for chaining
 */
off<K>(event: K, listener: EventListener<K>): this;

Usage Examples:

const myMap = SharedMap.create(runtime, "managed-events");

// Standard event listener
const valueChangeHandler = (changed, local) => {
  console.log(`Value changed: ${changed.key}`);
};

// Add listener
myMap.on("valueChanged", valueChangeHandler);

// One-time listener
myMap.once("valueChanged", (changed, local) => {
  console.log("First change detected!");
});

// Remove listener
myMap.off("valueChanged", valueChangeHandler);

// Chaining event setup
myMap
  .on("valueChanged", (changed, local) => {
    // Handle value changes
  })
  .on("clear", (local) => {
    // Handle clear events
  });

Event-Driven Patterns

Reactive Data Synchronization

// Create a reactive system that responds to remote changes
const gameState = SharedMap.create(runtime, "game");
const playersList = SharedDirectory.create(runtime, "players");

// Update UI when game state changes
gameState.on("valueChanged", (changed, local) => {
  if (!local) { // Only respond to remote changes
    switch (changed.key) {
      case "current-turn":
        updateCurrentPlayerUI(gameState.get("current-turn"));
        break;
      case "game-phase":
        updateGamePhaseUI(gameState.get("game-phase"));
        break;
      case "score":
        updateScoreUI(gameState.get("score"));
        break;
    }
  }
});

// Update player list when players join or leave
playersList.on("subDirectoryCreated", (path, local) => {
  if (!local) {
    const player = playersList.getWorkingDirectory(path);
    if (player) {
      addPlayerToUI(path, player.get("name"));
    }
  }
});

playersList.on("subDirectoryDeleted", (path, local) => {
  if (!local) {
    removePlayerFromUI(path);
  }
});

Change Logging and Debugging

// Comprehensive change logging system
const appData = SharedDirectory.create(runtime, "app");

// Log all changes with timestamps
appData.on("valueChanged", (changed, local) => {
  const timestamp = new Date().toISOString();
  const origin = local ? "LOCAL" : "REMOTE";
  console.log(`[${timestamp}] ${origin} VALUE: ${changed.path}/${changed.key}`);
  console.log(`  Previous: ${JSON.stringify(changed.previousValue)}`);
  console.log(`  Current: ${JSON.stringify(appData.getWorkingDirectory(changed.path)?.get(changed.key))}`);
});

// Log structural changes
appData.on("subDirectoryCreated", (path, local) => {
  const timestamp = new Date().toISOString();
  const origin = local ? "LOCAL" : "REMOTE";
  console.log(`[${timestamp}] ${origin} CREATE: ${path}`);
});

appData.on("subDirectoryDeleted", (path, local) => {
  const timestamp = new Date().toISOString();
  const origin = local ? "LOCAL" : "REMOTE";
  console.log(`[${timestamp}] ${origin} DELETE: ${path}`);
});

// Connection state logging
appData.on("connected", (isConnected, clientId) => {
  console.log(`[CONNECTION] ${isConnected ? "CONNECTED" : "DISCONNECTED"} - Client: ${clientId}`);
});

Conflict Detection and Resolution

// Detect and handle potential conflicts
const criticalData = SharedMap.create(runtime, "critical");
const lastKnownValues = new Map<string, any>();

criticalData.on("valueChanged", (changed, local) => {
  if (!local) {
    // Remote change detected
    const currentValue = criticalData.get(changed.key);
    const ourLastValue = lastKnownValues.get(changed.key);
    
    if (ourLastValue !== undefined && ourLastValue !== changed.previousValue) {
      // Potential conflict: our local cache differs from remote previous value
      console.warn(`Potential conflict detected for key: ${changed.key}`);
      console.warn(`Our cache: ${JSON.stringify(ourLastValue)}`);
      console.warn(`Remote previous: ${JSON.stringify(changed.previousValue)}`);
      console.warn(`Remote current: ${JSON.stringify(currentValue)}`);
      
      // Handle conflict (example: user notification)
      handleDataConflict(changed.key, ourLastValue, currentValue);
    }
  }
  
  // Update our cache
  lastKnownValues.set(changed.key, criticalData.get(changed.key));
});

function handleDataConflict(key: string, localValue: any, remoteValue: any) {
  // Implementation depends on application needs
  // Options: user prompt, automatic merge, use timestamp, etc.
}

Event Filtering and Routing

// Create a sophisticated event routing system
class DataEventRouter {
  private handlers = new Map<string, Array<(data: any) => void>>();
  
  constructor(private directory: ISharedDirectory) {
    // Set up global event listener
    directory.on("valueChanged", (changed, local) => {
      const routeKey = `${changed.path}/${changed.key}`;
      const handlers = this.handlers.get(routeKey);
      
      if (handlers) {
        const currentValue = this.directory.getWorkingDirectory(changed.path)?.get(changed.key);
        handlers.forEach(handler => {
          try {
            handler({ key: changed.key, path: changed.path, value: currentValue, local });
          } catch (error) {
            console.error(`Error in event handler for ${routeKey}:`, error);
          }
        });
      }
    });
  }
  
  // Subscribe to specific path/key combinations
  subscribe(path: string, key: string, handler: (data: any) => void) {
    const routeKey = `${path}/${key}`;
    if (!this.handlers.has(routeKey)) {
      this.handlers.set(routeKey, []);
    }
    this.handlers.get(routeKey)!.push(handler);
  }
  
  // Unsubscribe from specific handlers
  unsubscribe(path: string, key: string, handler: (data: any) => void) {
    const routeKey = `${path}/${key}`;
    const handlers = this.handlers.get(routeKey);
    if (handlers) {
      const index = handlers.indexOf(handler);
      if (index > -1) {
        handlers.splice(index, 1);
      }
    }
  }
}

// Usage
const appData = SharedDirectory.create(runtime, "app");
const router = new DataEventRouter(appData);

// Subscribe to specific data changes
router.subscribe("/users/alice", "status", (data) => {
  console.log(`Alice's status changed to: ${data.value}`);
});

router.subscribe("/settings", "theme", (data) => {
  console.log(`Theme changed to: ${data.value}`);
  updateApplicationTheme(data.value);
});

Best Practices

Event Listener Management

  • Always remove event listeners when components are disposed to prevent memory leaks
  • Use once() for one-time setup or initialization events
  • Be careful with event handler scope and this binding
  • Consider using event delegation patterns for managing many similar listeners

Performance Considerations

  • Avoid heavy computations in event handlers as they can block other events
  • Debounce or throttle high-frequency events if needed
  • Filter events early to avoid unnecessary processing
  • Use async handlers carefully to maintain event ordering

Error Handling

  • Wrap event handlers in try-catch blocks to prevent one handler from breaking others
  • Log errors appropriately but don't let them propagate up
  • Consider implementing fallback behavior for critical event handlers

Testing Event-Driven Code

  • Mock event emitters for unit testing
  • Test both local and remote event scenarios
  • Verify event ordering and timing in integration tests
  • Test error handling and edge cases in event handlers