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:
/**
* 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:
/**
* 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:
/**
* 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:
/**
* 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
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
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
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
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:
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:
Always unsubscribe from events when components unmount or observers are no longer needed
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) })
Consider batching updates to reduce notification frequency:
typescriptlet pendingChanges = [] store.onChange((uri, content, type) => { pendingChanges.push({ uri, type }) if (pendingChanges.length === 1) { setTimeout(() => { const changes = [...pendingChanges] pendingChanges = [] processBatchedChanges(changes) }, 100) } })
Use higher-level abstractions like React hooks or streams for declarative handling
Consider error handling in event observers to prevent unhandled exceptions