Skip to content

Event Types

This page documents the event system types used in the ReX content system for change notification and observation.

Content Change Events

The content system uses an event-based architecture to notify consumers of content changes:

typescript
/**
 * Types of content change events
 */
enum ContentChangeType {
  /**
   * Content was created
   */
  CREATED = 'created',

  /**
   * Content was updated
   */
  UPDATED = 'updated',

  /**
   * Content was deleted
   */
  DELETED = 'deleted',

  /**
   * Content was moved to a new URI
   */
  MOVED = 'moved',
}

/**
 * Interface for content change events
 */
interface ContentChangeEvent<T = string> {
  /**
   * URI of the content that changed
   */
  uri: string

  /**
   * Type of change
   */
  type: ContentChangeType

  /**
   * New content (for CREATED and UPDATED events)
   */
  content?: Content<T>

  /**
   * Previous content (for UPDATED and DELETED events)
   */
  previousContent?: Content<T>

  /**
   * Timestamp when the change occurred
   */
  timestamp: Date

  /**
   * Source of the change (adapter name, user, etc.)
   */
  source?: string
}

/**
 * Function type for observing content changes
 */
type ContentChangeObserver = <T = string>(
  uri: string,
  content: Content<T> | undefined,
  type: ContentChangeType,
  event?: ContentChangeEvent<T>
) => void

/**
 * Function to unsubscribe from observations
 */
type Unsubscribe = () => void

/**
 * Options for content observation
 */
interface ObserveOptions {
  /**
   * Pattern to filter URIs (glob syntax)
   */
  pattern?: string

  /**
   * Types of changes to observe
   */
  types?: ContentChangeType[]

  /**
   * Whether to include initial state in observation
   */
  includeInitial?: boolean

  /**
   * Debounce time in milliseconds
   */
  debounce?: number

  /**
   * Maximum number of events to process
   */
  limit?: number
}

Event Emitter Types

The system uses a simple event emitter for propagating changes:

typescript
/**
 * Type for event handlers
 */
type EventHandler<T = any> = (data: T) => void

/**
 * Interface for event emitters
 */
interface EventEmitter<T = any> {
  /**
   * Add an event listener
   * @param event Event name
   * @param handler Event handler
   * @returns Unsubscribe function
   */
  on(event: string, handler: EventHandler<T>): () => void

  /**
   * Remove an event listener
   * @param event Event name
   * @param handler Event handler
   */
  off(event: string, handler: EventHandler<T>): void

  /**
   * Emit an event
   * @param event Event name
   * @param data Event data
   */
  emit(event: string, data: T): void

  /**
   * Check if an event has listeners
   * @param event Event name
   */
  hasListeners(event: string): boolean

  /**
   * Remove all event listeners
   */
  removeAllListeners(): void
}

/**
 * Interface for content-specific event emitters
 */
interface ContentEventEmitter extends EventEmitter<ContentChangeEvent> {
  /**
   * Add a listener for all content changes
   * @param handler Event handler
   * @returns Unsubscribe function
   */
  onAny(handler: EventHandler<ContentChangeEvent>): () => void

  /**
   * Add a listener for changes to a specific URI
   * @param uri Content URI
   * @param handler Event handler
   * @returns Unsubscribe function
   */
  onUri(uri: string, handler: EventHandler<ContentChangeEvent>): () => void

  /**
   * Add a listener for a specific change type
   * @param type Change type
   * @param handler Event handler
   * @returns Unsubscribe function
   */
  onType(
    type: ContentChangeType,
    handler: EventHandler<ContentChangeEvent>
  ): () => void
}

Content Watcher Types

For filesystem-based storage, the system includes file watching capabilities:

typescript
/**
 * Types of file system events
 */
enum FileSystemEventType {
  /**
   * File was added
   */
  ADDED = 'added',

  /**
   * File was modified
   */
  MODIFIED = 'modified',

  /**
   * File was deleted
   */
  DELETED = 'deleted',
}

/**
 * Interface for file system events
 */
interface FileSystemEvent {
  /**
   * Type of event
   */
  type: FileSystemEventType

  /**
   * Path to the file or directory
   */
  path: string

  /**
   * Whether the path is a directory
   */
  isDirectory?: boolean

  /**
   * Timestamp when the event occurred
   */
  timestamp: Date
}

/**
 * Function type for observing file system changes
 */
type FileSystemWatchHandler = (event: FileSystemEvent) => void

/**
 * Interface for file system watchers
 */
interface FileSystemWatcher {
  /**
   * Start watching
   * @param path Path to watch
   * @param options Watch options
   * @returns Watcher instance
   */
  watch(path: string, options?: FileSystemWatchOptions): FileSystemWatcher

  /**
   * Add a change handler (preferred method)
   * @param handler Watch handler
   * @param options Observe options
   * @returns Unsubscribe function
   */
  observe(
    handler: FileSystemWatchHandler,
    options?: FileSystemObserveOptions
  ): Unsubscribe

  /**
   * Add a change handler (legacy method)
   * @param handler Watch handler
   * @returns Unsubscribe function
   * @deprecated Use observe() instead
   */
  onChange(handler: FileSystemWatchHandler): Unsubscribe

  /**
   * Stop watching
   */
  close(): void
}

/**
 * Options for file system watching
 */
interface FileSystemWatchOptions {
  /**
   * Whether to watch subdirectories
   */
  recursive?: boolean

  /**
   * File patterns to include
   */
  include?: string | string[]

  /**
   * File patterns to exclude
   */
  exclude?: string | string[]

  /**
   * Whether to ignore initial scan events
   */
  ignoreInitial?: boolean

  /**
   * Polling interval in milliseconds
   */
  pollInterval?: number

  /**
   * Whether to use polling instead of native events
   */
  usePolling?: boolean
}

/**
 * Options for file system observation
 */
interface FileSystemObserveOptions {
  /**
   * Types of events to observe
   */
  types?: FileSystemEventType[]

  /**
   * Pattern to filter paths (glob syntax)
   */
  pattern?: string

  /**
   * Whether to include initial state in observation
   */
  includeInitial?: boolean

  /**
   * Debounce time in milliseconds
   */
  debounce?: number
}

Stream Interfaces

For reactive programming, the system provides stream-based interfaces:

typescript
/**
 * Interface for a content stream
 */
interface ContentStream<T = string> {
  /**
   * Subscribe to the stream
   * @param observer Stream observer
   * @returns Unsubscribe function
   */
  subscribe(observer: ContentStreamObserver<T>): () => void

  /**
   * Map the stream to a new stream
   * @param mapper Mapping function
   * @returns New stream
   */
  map<R>(mapper: (content: Content<T>) => Content<R>): ContentStream<R>

  /**
   * Filter the stream
   * @param predicate Filter function
   * @returns New stream
   */
  filter(predicate: (content: Content<T>) => boolean): ContentStream<T>

  /**
   * Merge with another stream
   * @param other Other stream
   * @returns Merged stream
   */
  merge<R>(other: ContentStream<R>): ContentStream<T | R>

  /**
   * Get the current value
   */
  getValue(): Content<T> | undefined
}

/**
 * Interface for content stream observers
 */
interface ContentStreamObserver<T = string> {
  /**
   * Handle next content value
   * @param content Content value
   */
  next(content: Content<T>): void

  /**
   * Handle error
   * @param error Error
   */
  error?(error: Error): void

  /**
   * Handle completion
   */
  complete?(): void
}

Usage Examples

Basic Change Observation

typescript
import { createContentStore } from '@lib/content'
import { ContentChangeType } from '@lib/content/events'

// Create a store
const store = createContentStore()

// Subscribe to all changes using observe pattern
const unsubscribe = store.observe(
  (uri, content, type, event) => {
    console.log(`Change detected: ${type} at ${uri}`)

    if (content) {
      console.log(`Title: ${content.metadata.title}`)
    }

    if (event?.previousContent) {
      console.log(`Previous title: ${event.previousContent.metadata.title}`)
    }
  },
  {
    // Optional pattern to filter content by URI pattern
    pattern: '**/*.md',
    // Optional filter by change types
    types: [ContentChangeType.CREATED, ContentChangeType.UPDATED],
  }
)

// Make some changes
await store.write('doc.md', {
  data: '# Document',
  contentType: 'text/markdown',
  metadata: { title: 'Document' },
})

await store.write('doc.md', {
  data: '# Updated Document',
  contentType: 'text/markdown',
  metadata: { title: 'Updated Document' },
})

await store.delete('doc.md')

// Unsubscribe when done
unsubscribe()

Targeted Change Observation

typescript
import { createContentStore } from '@lib/content'
import { ContentChangeType } from '@lib/content/events'

// Create a store with a custom event emitter
const store = createContentStore({
  adapterFactory: () =>
    createMemoryAdapter({
      events: createContentEventEmitter(),
    }),
})

// Get the event emitter
const emitter = (store as any).adapter.events as ContentEventEmitter

// Subscribe to a specific URI
const unsubscribeUri = emitter.onUri('blog/post-1.md', event => {
  console.log(`Change to blog/post-1.md: ${event.type}`)
})

// Subscribe to a specific event type
const unsubscribeType = emitter.onType(ContentChangeType.UPDATED, event => {
  console.log(`Content updated: ${event.uri}`)
})

// Make some changes
await store.write('blog/post-1.md', {
  data: '# Post 1',
  contentType: 'text/markdown',
  metadata: { title: 'Post 1' },
})

await store.write('blog/post-2.md', {
  data: '# Post 2',
  contentType: 'text/markdown',
  metadata: { title: 'Post 2' },
})

await store.write('blog/post-1.md', {
  data: '# Updated Post 1',
  contentType: 'text/markdown',
  metadata: { title: 'Updated Post 1' },
})

// Unsubscribe when done
unsubscribeUri()
unsubscribeType()

File System Watching

typescript
import { createFileSystemWatcher } from '@lib/content/adapters/node'
import { FileSystemEventType } from '@lib/content/events'

// Create a watcher
const watcher = createFileSystemWatcher()

// Start watching a directory
watcher.watch('/path/to/content', {
  recursive: true,
  include: ['*.md', '*.mdx'],
  exclude: ['node_modules', '.git'],
  ignoreInitial: true,
})

// Subscribe to changes using observe pattern
const unsubscribe = watcher.observe(
  event => {
    console.log(`File system event: ${event.type} ${event.path}`)

    if (
      event.type === FileSystemEventType.ADDED ||
      event.type === FileSystemEventType.MODIFIED
    ) {
      console.log(`Processing updated file: ${event.path}`)
    } else if (event.type === FileSystemEventType.DELETED) {
      console.log(`Removing deleted file: ${event.path}`)
    }
  },
  {
    // Only observe specific event types
    types: [FileSystemEventType.ADDED, FileSystemEventType.MODIFIED],
    // Only observe files matching pattern
    pattern: '**/*.{md,mdx}',
    // Debounce to avoid excessive processing
    debounce: 100,
  }
)

// Stop watching when done
setTimeout(() => {
  unsubscribe()
  watcher.close()
}, 60000)

Reactive Content Streams

typescript
import { createContentStream } from '@lib/content/streams'
import { createContentStore } from '@lib/content'

// Create a store
const store = createContentStore()

// Create a stream for a specific URI
const postStream = createContentStream('blog/post.md', store)

// Subscribe to the stream
const unsubscribe = postStream.subscribe({
  next: content => {
    console.log(`Received update: ${content.metadata.title}`)
    updateUI(content)
  },
  error: error => {
    console.error(`Stream error: ${error.message}`)
    showErrorUI(error)
  },
})

// Transform the stream
const htmlStream = postStream
  .filter(content => content.contentType === 'text/markdown')
  .map(content => ({
    ...content,
    data: markdownToHtml(content.data),
    contentType: 'text/html',
  }))

// Subscribe to the transformed stream
const unsubscribeHtml = htmlStream.subscribe({
  next: content => {
    document.getElementById('preview').innerHTML = content.data
  },
})

// Make a change to trigger the stream
await store.write('blog/post.md', {
  data: '# My Post\n\nContent here',
  contentType: 'text/markdown',
  metadata: { title: 'My Post', updatedAt: new Date() },
})

// Unsubscribe when done
unsubscribe()
unsubscribeHtml()

React Integration

The system provides React hooks for integrating with the event system:

typescript
import { useContent, useContentObserver } from '@lib/content/react';
import { ContentChangeType } from '@lib/content/events';

function BlogPost({ uri }) {
  // Get content with automatic updates
  const { content, loading, error } = useContent(uri);

  // Subscribe to specific change types using the observer pattern
  useContentObserver(
    (content, type) => {
      if (type === ContentChangeType.UPDATED) {
        notifyUser(`Post "${content.metadata.title}" was updated`);
      }
    },
    {
      uri,
      types: [ContentChangeType.UPDATED],
      debounce: 500
    }
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>{content.metadata.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: renderMarkdown(content.data) }} />
    </div>
  );
}

Best Practices

When working with content events:

  1. Always unsubscribe from events when components unmount or observers are no longer needed

  2. Be selective with what you observe to avoid performance issues:

    typescript
    // Good: Use ObserveOptions to filter at the source
    store.observe(
      (uri, content, type) => {
        // Handler only receives blog updates
        handleBlogUpdate(content)
      },
      {
        pattern: 'blog/**/*',
        types: [ContentChangeType.UPDATED],
      }
    )
    
    // Bad: Filter after receiving all events
    store.observe((uri, content, type) => {
      // Receives all events but only processes some
      if (uri.startsWith('blog/') && type === ContentChangeType.UPDATED) {
        handleBlogUpdate(content)
      }
    })
    
    // Worse: Process all changes unnecessarily without filtering
    store.observe((uri, content, type) => {
      // Expensive processing for all changes
      expensiveOperation(uri, content, type)
    })
  3. Consider batching updates to reduce notification frequency:

    typescript
    let pendingChanges = []
    
    store.onChange((uri, content, type) => {
      pendingChanges.push({ uri, type })
    
      if (pendingChanges.length === 1) {
        setTimeout(() => {
          const changes = [...pendingChanges]
          pendingChanges = []
    
          processBatchedChanges(changes)
        }, 100)
      }
    })
  4. Use higher-level abstractions like React hooks or streams for declarative handling

  5. Consider error handling in event observers to prevent unhandled exceptions

Released under the MIT License.