Architecture Refinement Recommendations
This document addresses architectural challenges identified in the ReX content system and proposes refinement strategies to enhance clarity, cohesion, and extensibility.
1. Layer Boundaries and Responsibilities
Issue: Unclear Boundaries Between Layers
The current architecture has somewhat blurred boundaries between core, middleware, adapter, and specialized content layers, leading to inconsistent implementation patterns.
Recommended Strategy
Define clear layer boundaries and responsibilities:
- Document the architectural layers:
Content System Layers:
┌─────────────────────────────────────┐
│ Application Layer │
│ - React hooks, components │
│ - Framework integration │
│ - UI-specific transformations │
└─────────────────┬───────────────────┘
│
┌─────────────────▼───────────────────┐
│ Content Store Layer │
│ - High-level content operations │
│ - Content lifecycle management │
│ - Event handling and observation │
└─────────────────┬───────────────────┘
│
┌─────────────────▼───────────────────┐
│ Middleware Layer │
│ - Cross-cutting concerns │
│ - Feature composition │
│ - Transformation and validation │
└─────────────────┬───────────────────┘
│
┌─────────────────▼───────────────────┐
│ Adapter Layer │
│ - Storage mechanism abstraction │
│ - Environment-specific operations │
│ - Raw content handling │
└─────────────────┬───────────────────┘
│
┌─────────────────▼───────────────────┐
│ Storage Layer │
│ - Physical storage mechanisms │
│ - File system, IndexedDB, etc. │
│ - External content services │
└─────────────────────────────────────┘
- Define clear responsibilities for each layer:
// Application Layer: Integrates with UI frameworks
interface ContentHook {
content: Content | null
loading: boolean
error: Error | null
update(content: Content): Promise<void>
delete(): Promise<void>
refresh(): Promise<void>
}
// Store Layer: High-level content operations
interface ContentStore {
read(uri: string, options?: ReadOptions): Promise<Content>
write(uri: string, content: Content, options?: WriteOptions): Promise<void>
delete(uri: string, options?: DeleteOptions): Promise<void>
list(pattern?: string, options?: ListOptions): Promise<string[]>
observe(callback: ContentObserver, options?: ObserveOptions): Unsubscribe
}
// Middleware Layer: Cross-cutting concerns
type Middleware<T extends Function> = (next: T) => T
// Adapter Layer: Storage abstraction
interface ContentAdapter {
read(uri: string): Promise<RawContent>
write(uri: string, content: RawContent): Promise<void>
delete(uri: string): Promise<void>
list(pattern?: string): Promise<string[]>
// Optional methods...
}
// Storage Layer: Direct storage mechanisms
// Specific to implementation environment
- Create clear transformation patterns between layers:
// Store to Application Layer transformation
function useContent(uri: string): ContentHook {
const store = useContentStore()
const [content, setContent] = useState<Content | null>(null)
const [loading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<Error | null>(null)
// Implementation details...
return {
content,
loading,
error,
update: content => store.write(uri, content),
delete: () => store.delete(uri),
refresh: () => loadContent(),
}
}
// Store to Middleware transformation
function createEnhancedStore(baseStore: ContentStore): ContentStore {
return compose(
baseStore,
withValidation(schema),
withCaching(options),
withLogging(options)
)
}
// Middleware to Adapter transformation
function createAdapter(options: AdapterOptions): ContentAdapter {
const baseAdapter = createBaseAdapter(options)
return compose(baseAdapter, withErrorHandling(), withMetrics())
}
This approach clarifies the responsibilities of each layer and establishes clear patterns for communication between layers.
2. Adapter Capability Discovery
Issue: Inconsistent Adapter Capabilities
Adapters have inconsistent capabilities, with optional methods that may or may not be implemented, leading to uncertainty in usage.
Recommended Strategy
Implement a formal capability discovery system:
- Define capability interfaces:
// Base capabilities interface
interface AdapterCapabilities {
// Core capabilities (always true for valid adapters)
canRead: true
canWrite: true
canDelete: true
canList: true
// Optional capabilities
canWatch?: boolean
canMove?: boolean
canCopy?: boolean
canGetMetadata?: boolean
canCreateDirectory?: boolean
// Other capabilities...
}
// Extended interface for file system adapters
interface FileSystemCapabilities extends AdapterCapabilities {
canCreateDirectory: boolean
canReadDirectory: boolean
canGetStats: boolean
}
// Extended interface for browser storage adapters
interface BrowserStorageCapabilities extends AdapterCapabilities {
canPersist: boolean
canGetQuota: boolean
canWatch: boolean
}
- Add capability detection to adapter interface:
interface ContentAdapter {
// Core methods...
// Capability discovery
getCapabilities(): AdapterCapabilities
// Type guard helper
hasCapability<K extends keyof AdapterCapabilities>(
capability: K
): this is ContentAdapter & { [P in K]: true }
}
- Implement capability checking in stores:
class ContentStore {
private adapter: ContentAdapter
constructor(adapter: ContentAdapter) {
this.adapter = adapter
}
async move(source: string, destination: string): Promise<void> {
if (this.adapter.hasCapability('canMove')) {
// Use native implementation
return this.adapter.move!(source, destination)
}
// Fallback implementation
const content = await this.adapter.read(source)
await this.adapter.write(destination, content)
await this.adapter.delete(source)
}
// Other methods with capability checks...
}
- Document capability requirements for middleware:
// Example of middleware documenting requirements
function withVersioning(
options?: VersioningOptions
): Middleware<ContentAdapter> {
// Document required capabilities
const requiredCapabilities: (keyof AdapterCapabilities)[] = [
'canRead',
'canWrite',
]
return next => {
// Verify capabilities
for (const capability of requiredCapabilities) {
if (!next.hasCapability(capability)) {
throw new Error(
`Versioning middleware requires the ${capability} capability`
)
}
}
// Middleware implementation
return {
// Enhanced methods...
}
}
}
This approach provides clear capability detection and graceful fallbacks for missing capabilities.
3. Composition Architecture Enhancement
Issue: Composition Pattern Limitations
The current composition pattern has some limitations in handling complex middleware and error propagation.
Recommended Strategy
Enhance the composition pattern with middleware contexts and error handling:
- Add middleware context for sharing state:
// Middleware context for sharing state
interface MiddlewareContext {
// Core context properties
readonly adapter: ContentAdapter
readonly options: Record<string, any>
// State management
getState<T>(key: string): T | undefined
setState<T>(key: string, value: T): void
// Metadata
readonly middlewareChain: string[]
}
// Enhanced middleware type
type EnhancedMiddleware<T extends Function> = (
next: T,
context: MiddlewareContext
) => T
// Enhanced composition function
function composeWithContext<T extends Function>(
base: T,
middlewares: EnhancedMiddleware<T>[],
options?: Record<string, any>
): T {
const context: MiddlewareContext = {
adapter: base as any,
options: options || {},
middlewareChain: middlewares.map(m => m.name || 'anonymous'),
// State management
state: new Map<string, any>(),
getState<V>(key: string): V | undefined {
return this.state.get(key) as V | undefined
},
setState<V>(key: string, value: V): void {
this.state.set(key, value)
},
}
return middlewares.reduce(
(enhanced, middleware) => middleware(enhanced, context),
base
)
}
- Implement better error handling in middleware:
// Error handling middleware factory
function withErrorHandling(
options?: ErrorHandlingOptions
): Middleware<ContentAdapter> {
return (next, context) => ({
async read(uri: string): Promise<Content> {
try {
return await next.read(uri)
} catch (error) {
// Enhance error with context
const enhancedError = enhanceError(error, {
operation: 'read',
uri,
middleware: 'ErrorHandlingMiddleware',
middlewareChain: context.middlewareChain,
})
// Handle or transform error based on options
if (options?.onError) {
return options.onError(enhancedError, 'read', uri)
}
// Re-throw enhanced error
throw enhancedError
}
},
// Similar pattern for other methods
})
}
// Utility to enhance errors with context
function enhanceError(error: any, context: Record<string, any>): ContentError {
if (error instanceof ContentError) {
// Add context to existing error
error.addContext(context)
return error
}
// Convert to ContentError
return new ContentError(error.message || 'Unknown error', error, context)
}
- Create composition recipes for common patterns:
// Common middleware combinations
const createStandardStore = (
adapter: ContentAdapter,
options?: StoreOptions
) => {
return createStore(
composeWithContext(
adapter,
[
withValidation(options?.schema),
withErrorHandling(options?.errorHandling),
withLogging(options?.logging),
withCaching(options?.caching),
withMetrics(options?.metrics),
],
options
)
)
}
// Environment-specific recipes
const createBrowserStore = (options?: BrowserStoreOptions) => {
const adapter = createIndexedDBAdapter(options?.storage)
return createStandardStore(
composeWithContext(
adapter,
[withOfflineSupport(options?.offline), withSyncThrottling(options?.sync)],
options
)
)
}
This approach enhances the composition pattern with better context sharing and error handling.
4. Cross-Environment Architecture
Issue: Inconsistent Environment Handling
The current architecture has inconsistent patterns for handling environment-specific code.
Recommended Strategy
Create a unified environment detection and adaptation system:
- Define environment detection interface:
// Environment detection API
interface Environment {
// Environment type detection
isNode: boolean
isBrowser: boolean
isServiceWorker: boolean
isReactNative: boolean
isDeno: boolean
// Feature detection
hasFeature(featureName: string): boolean
// Environment-specific capabilities
capabilities: {
fs: boolean // File system access
indexedDB: boolean // IndexedDB support
localStorage: boolean // LocalStorage support
serviceWorker: boolean // ServiceWorker API
fetch: boolean // Fetch API
// Other capabilities
}
// Environment metadata
meta: {
platform: string
version: string
userAgent?: string
// Other metadata
}
}
- Implement environment-aware factory functions:
// Environment-aware store factory
function createContentStore(options?: ContentStoreOptions): ContentStore {
const env = detectEnvironment()
// Select appropriate adapter based on environment
let adapter: ContentAdapter
if (env.isNode && env.hasFeature('fs')) {
adapter = createFileSystemAdapter(options?.fs)
} else if (env.isBrowser && env.hasFeature('indexedDB')) {
adapter = createIndexedDBAdapter(options?.indexedDB)
} else if (env.isBrowser && env.hasFeature('localStorage')) {
adapter = createLocalStorageAdapter(options?.localStorage)
} else {
adapter = createMemoryAdapter(options?.memory)
}
// Apply environment-specific middleware
const middlewares = [
withErrorHandling(options?.errorHandling),
withValidation(options?.validation),
...(options?.middleware || []),
]
if (env.isBrowser && options?.offline) {
middlewares.push(withOfflineSupport(options.offline))
}
if (env.isNode && options?.watching) {
middlewares.push(withFileWatching(options.watching))
}
return createStore(composeWithContext(adapter, middlewares, options))
}
- Implement isomorphic feature detection:
// Feature detection utilities
const Environment = {
// Detect current environment
detect(): Environment {
const isNode =
typeof process !== 'undefined' &&
process.versions != null &&
process.versions.node != null
const isBrowser =
typeof window !== 'undefined' && typeof window.document !== 'undefined'
const isServiceWorker =
typeof self === 'object' &&
self.constructor &&
self.constructor.name === 'ServiceWorkerGlobalScope'
// Feature detection
const capabilities = {
fs: isNode && this.hasRequire('fs'),
indexedDB: typeof indexedDB !== 'undefined',
localStorage: typeof localStorage !== 'undefined',
serviceWorker:
typeof navigator !== 'undefined' && 'serviceWorker' in navigator,
fetch: typeof fetch !== 'undefined',
// Other capabilities
}
return {
isNode,
isBrowser,
isServiceWorker,
isReactNative:
typeof navigator !== 'undefined' && navigator.product === 'ReactNative',
isDeno: typeof Deno !== 'undefined',
hasFeature(feature: string): boolean {
return !!capabilities[feature as keyof typeof capabilities]
},
capabilities,
meta: {
platform: this.getPlatform(),
version: this.getVersion(),
userAgent:
typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
},
}
},
// Helper to safely check for Node.js require
hasRequire(module: string): boolean {
try {
return typeof require === 'function' && !!require.resolve(module)
} catch (e) {
return false
}
},
// Get platform information
getPlatform(): string {
if (typeof process !== 'undefined' && process.platform) {
return process.platform
}
if (typeof navigator !== 'undefined') {
return navigator.platform || 'browser'
}
return 'unknown'
},
// Get version information
getVersion(): string {
if (typeof process !== 'undefined' && process.versions?.node) {
return `Node.js ${process.versions.node}`
}
if (typeof navigator !== 'undefined' && navigator.userAgent) {
return navigator.userAgent
}
return 'unknown'
},
}
This approach provides a consistent way to handle environment-specific code across the system.
5. Plugin Architecture
Issue: Limited Extension Points
The current architecture lacks a formal plugin system for extending functionality.
Recommended Strategy
Implement a modular plugin system:
- Define a plugin interface:
// Plugin interface
interface ContentPlugin<T = any> {
// Plugin metadata
readonly name: string
readonly version: string
readonly dependencies?: string[]
// Lifecycle hooks
onInstall(registry: PluginRegistry): void | Promise<void>
onUninstall?(): void | Promise<void>
// Plugin-specific functionality
getFeatures(): Record<string, any>
// Plugin configuration
configure?(options: T): void
}
// Plugin registry
interface PluginRegistry {
// Plugin registration
register(plugin: ContentPlugin): void
unregister(pluginName: string): void
// Plugin discovery
getPlugin<T extends ContentPlugin>(name: string): T | undefined
hasPlugin(name: string): boolean
listPlugins(): Array<{ name: string; version: string }>
// Feature access
getFeature<T>(pluginName: string, featureName: string): T | undefined
// System integration
getStore(): ContentStore
getAdapter(): ContentAdapter
// Events
on(event: PluginEvent, callback: (data: any) => void): () => void
}
// Plugin event types
type PluginEvent =
| 'plugin:registered'
| 'plugin:unregistered'
| 'feature:added'
| 'feature:removed'
- Implement a plugin registry:
class DefaultPluginRegistry implements PluginRegistry {
private plugins: Map<string, ContentPlugin> = new Map()
private eventEmitter = new EventEmitter()
private store: ContentStore
private adapter: ContentAdapter
constructor(store: ContentStore, adapter: ContentAdapter) {
this.store = store
this.adapter = adapter
}
async register(plugin: ContentPlugin): Promise<void> {
if (this.plugins.has(plugin.name)) {
throw new Error(`Plugin "${plugin.name}" is already registered`)
}
// Check dependencies
if (plugin.dependencies) {
for (const dep of plugin.dependencies) {
if (!this.plugins.has(dep)) {
throw new Error(
`Plugin "${plugin.name}" depends on "${dep}" which is not registered`
)
}
}
}
// Install plugin
await plugin.onInstall?.(this)
// Register plugin
this.plugins.set(plugin.name, plugin)
this.eventEmitter.emit('plugin:registered', { name: plugin.name })
}
// Other methods...
}
- Create plugin-based extensions:
// Example validation plugin
const validationPlugin: ContentPlugin<ValidationOptions> = {
name: 'validation',
version: '1.0.0',
// Configuration
options: {
schemas: new Map(),
},
configure(options: ValidationOptions): void {
this.options = { ...this.options, ...options }
},
// Installation
onInstall(registry: PluginRegistry): void {
const store = registry.getStore()
const originalWrite = store.write.bind(store)
// Enhance the store's write method
Object.defineProperty(store, 'write', {
value: async (uri: string, content: Content, options?: WriteOptions) => {
// Get schema for content type
const schema = this.options.schemas.get(content.contentType)
// Validate if schema exists
if (schema) {
const result = validate(content, schema)
if (!result.valid) {
throw new ValidationError(uri, result.errors)
}
}
// Proceed with original method
return originalWrite(uri, content, options)
},
})
},
// Features
getFeatures(): Record<string, any> {
return {
validate: (content: Content, schemaName: string) => {
const schema = this.options.schemas.get(schemaName)
if (!schema) {
throw new Error(`Schema "${schemaName}" not found`)
}
return validate(content, schema)
},
addSchema: (name: string, schema: Schema) => {
this.options.schemas.set(name, schema)
},
}
},
}
- Document plugin system architecture:
Plugin System Architecture:
┌───────────────────────────────────────────────────────────────┐
│ Content System │
│ │
│ ┌─────────────────────────┐ ┌─────────────────────┐ │
│ │ │ │ │ │
│ │ Content Store │◄───────┤ Plugin Registry │ │
│ │ │ │ │ │
│ └───────────┬─────────────┘ └─────────┬───────────┘ │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────────────────┐ ┌─────────────────────┐ │
│ │ │ │ │ │
│ │ Middleware Pipeline │◄───────┤ Plugin System │ │
│ │ │ │ │ │
│ └───────────┬───────────┘ └─────────────────────┘ │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────────────────┐ ┌─────────────────────┐ │
│ │ │ │ │ │
│ │ Content Adapter │◄───────┤ Extension Points │ │
│ │ │ │ │ │
│ └───────────────────────┘ └─────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────┘
This approach provides a formalized plugin system for extending the content system with new functionality.
6. Error Handling Architecture
Issue: Inconsistent Error Handling
The current error handling is inconsistent across layers, making it difficult to trace and handle errors properly.
Recommended Strategy
Implement a comprehensive error system:
- Define a consistent error hierarchy:
// Base error class
class ContentError extends Error {
// Error metadata
readonly code: string
readonly uri?: string
readonly operation?: string
readonly cause?: Error
readonly context: Record<string, any>
constructor(
message: string,
options?: {
code?: string
uri?: string
operation?: string
cause?: Error
context?: Record<string, any>
}
) {
super(message)
this.name = this.constructor.name
this.code = options?.code || 'UNKNOWN_ERROR'
this.uri = options?.uri
this.operation = options?.operation
this.cause = options?.cause
this.context = options?.context || {}
// Capture stack trace
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor)
}
}
// Add context to error
addContext(context: Record<string, any>): this {
this.context = { ...this.context, ...context }
return this
}
// Convert to JSON for logging
toJSON(): Record<string, any> {
return {
name: this.name,
message: this.message,
code: this.code,
uri: this.uri,
operation: this.operation,
context: this.context,
cause: this.cause
? this.cause instanceof ContentError
? this.cause.toJSON()
: this.cause.message
: undefined,
stack: this.stack,
}
}
}
// Specific error types
class ContentNotFoundError extends ContentError {
constructor(uri: string, cause?: Error) {
super(`Content not found: ${uri}`, {
code: 'CONTENT_NOT_FOUND',
uri,
operation: 'read',
cause,
})
}
}
class ContentValidationError extends ContentError {
readonly errors: any[]
constructor(uri: string, errors: any[]) {
super(
`Validation failed for ${uri}: ${errors.map(e => e.message).join(', ')}`,
{
code: 'VALIDATION_ERROR',
uri,
operation: 'write',
}
)
this.errors = errors
}
toJSON(): Record<string, any> {
return {
...super.toJSON(),
errors: this.errors,
}
}
}
// Additional error types...
- Implement consistent error handling middleware:
// Error handling middleware
function withErrorHandling(
options?: ErrorHandlingOptions
): Middleware<ContentAdapter> {
return next => ({
async read(uri: string): Promise<Content> {
try {
return await next.read(uri)
} catch (error) {
// Transform standard errors to content errors
if (error.code === 'ENOENT') {
throw new ContentNotFoundError(uri, error)
}
// Enhance existing content errors
if (error instanceof ContentError) {
error.addContext({ operation: 'read', uri })
throw error
}
// Create generic content error
throw new ContentError(error.message || 'Read operation failed', {
code: 'READ_ERROR',
uri,
cause: error,
})
}
},
// Similar implementations for other methods
})
}
- Create error handling utilities:
// Error utilities
const ErrorUtils = {
// Check if error is a specific type
isContentError(error: any): error is ContentError {
return error instanceof ContentError
},
isNotFoundError(error: any): error is ContentNotFoundError {
return error instanceof ContentNotFoundError
},
// Convert any error to ContentError
toContentError(error: unknown, context?: Record<string, any>): ContentError {
if (error instanceof ContentError) {
if (context) {
error.addContext(context)
}
return error
}
if (error instanceof Error) {
return new ContentError(error.message, {
cause: error,
context,
})
}
return new ContentError(
typeof error === 'string' ? error : 'Unknown error',
{ context }
)
},
// Create error handler with recovery options
createErrorHandler(options?: ErrorHandlerOptions) {
return (error: unknown, operation: string, uri?: string) => {
const contentError = this.toContentError(error, { operation, uri })
// Log error
if (options?.logErrors !== false) {
console.error('Content system error:', contentError)
}
// Execute custom handler if provided
if (options?.onError) {
return options.onError(contentError)
}
// Retry logic
if (options?.retry && options.retry.attempts > 0) {
// Implementation of retry logic
}
// Re-throw by default
throw contentError
}
},
}
- Document error handling patterns:
// Example usage in application code
try {
const content = await contentStore.read('blog/post.md')
// Process content
} catch (error) {
if (ErrorUtils.isNotFoundError(error)) {
// Handle not found specifically
return showNotFoundPage()
}
if (error instanceof ContentValidationError) {
// Handle validation errors
return showValidationErrors(error.errors)
}
// Generic error handling
logError(error)
showErrorMessage('Failed to load content')
}
This approach provides a consistent error handling pattern throughout the system, making errors more traceable and recoverable.
Implementation Roadmap
Phase 1: Architecture Documentation
- Document the layer boundaries and responsibilities
- Create architecture diagrams showing component relationships
- Define clear interfaces for each layer
- Document transformation patterns between layers
Phase 2: Core Architecture Refinement
- Implement capability discovery system
- Enhance composition pattern with context and error handling
- Create environment detection and adaptation system
- Develop standardized error handling architecture
Phase 3: Extension Architecture
- Design and implement plugin system
- Create extension points for core functionality
- Develop plugin registry and discovery mechanisms
- Document plugin development patterns
Phase 4: Implementation and Migration
- Refactor core components to align with new architecture
- Implement reference adapters with consistent patterns
- Create middleware examples following best practices
- Develop migration guides for existing code
Conclusion
The proposed architectural refinements address key challenges in the current ReX content system while preserving its core strengths. By clarifying layer boundaries, standardizing patterns, and enhancing extensibility, these changes will:
- Improve code organization and maintainability
- Reduce developer confusion when working across layers
- Enable more consistent error handling and recovery
- Provide clear extension points for future growth
- Support a broader range of environments and use cases
Implementing these recommendations will create a more cohesive and robust architecture that better supports the system’s goals of flexibility, composability, and isomorphic operation.