npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@bernierllc/content-editor-service

v0.1.5

Published

Editor orchestration and coordination service for content management

Readme

@bernierllc/content-editor-service

Editor orchestration and coordination service for content management.

Overview

This package provides a service layer for orchestrating content editing operations, including auto-save management, soft delete functionality, version history, and editor session management. It coordinates between the content type registry, auto-save manager, and soft delete manager to provide a unified editing experience.

Features

  • Editor Session Management: Start and end editor sessions with state tracking
  • Auto-Save Coordination: Integrate with auto-save manager for seamless saving
  • Soft Delete Support: Handle soft delete operations with state management
  • Version History: Track content versions with metadata
  • Content Validation: Validate content against content type schemas
  • Event System: Comprehensive event system for editor operations
  • Statistics Tracking: Track usage statistics and performance metrics
  • TypeScript Support: Full type safety and IntelliSense

Installation

npm install @bernierllc/content-editor-service

Quick Start

import { ContentEditorService } from '@bernierllc/content-editor-service';

// Create editor service
const service = new ContentEditorService({
  enabled: true,
  autoSave: {
    enabled: true,
    debounceMs: 1000,
    storageMode: 'both'
  },
  softDelete: {
    enabled: true,
    showDeletedToUsers: false
  },
  versionHistory: {
    enabled: true,
    maxVersions: 50
  }
});

// Initialize the service
await service.initialize();

// Start an editor session
const editorState = await service.startEditorSession({
  contentId: 'content-1',
  contentTypeId: 'text',
  userId: 'user-123',
  userPermissions: ['content.edit'],
  contentMetadata: { type: 'text', title: 'My Article' }
});

// Save content
const saveResult = await service.saveContent({
  contentId: 'content-1',
  contentTypeId: 'text',
  contentData: {
    id: 'content-1',
    data: { title: 'My Article', content: 'Article content...' },
    metadata: { type: 'text' },
    lastModified: new Date(),
    version: 1
  },
  saveType: 'draft',
  userId: 'user-123',
  reason: 'Saving draft',
  createVersion: true
});

if (saveResult.success) {
  console.log('Content saved successfully');
}

// End the editor session
await service.endEditorSession(editorState.session.id);

API Reference

ContentEditorService

The main service class for managing content editing operations.

Constructor

new ContentEditorService(config?: Partial<ContentEditorServiceConfig>)

Configuration Options

interface ContentEditorServiceConfig {
  enabled: boolean;                    // Whether the service is enabled
  autoSave: {
    enabled: boolean;                  // Whether auto-save is enabled
    debounceMs: number;               // Debounce delay in milliseconds
    storageMode: 'local' | 'server' | 'both'; // Storage mode
  };
  softDelete: {
    enabled: boolean;                  // Whether soft delete is enabled
    showDeletedToUsers: boolean;      // Whether to show deleted content to users
  };
  versionHistory: {
    enabled: boolean;                  // Whether to track version history
    maxVersions: number;              // Maximum number of versions to keep
  };
  validation: {
    enabled: boolean;                  // Whether to validate content
    validateOnAutoSave: boolean;      // Whether to validate on auto-save
  };
}

Methods

initialize()

Initialize the service and set up storage adapters.

await service.initialize();
startEditorSession(context)

Start a new editor session for content editing.

const editorState = await service.startEditorSession({
  contentId: 'content-1',
  contentTypeId: 'text',
  userId: 'user-123',
  userPermissions: ['content.edit'],
  contentMetadata: { type: 'text', title: 'My Article' }
});
endEditorSession(sessionId)

End an editor session and clean up resources.

await service.endEditorSession(sessionId);
saveContent(operation)

Save content with optional version creation.

const result = await service.saveContent({
  contentId: 'content-1',
  contentTypeId: 'text',
  contentData: contentData,
  saveType: 'draft', // 'draft' | 'publish' | 'auto-save'
  userId: 'user-123',
  reason: 'Saving draft',
  createVersion: true
});
loadContent(operation)

Load content with optional version specification.

const result = await service.loadContent({
  contentId: 'content-1',
  contentTypeId: 'text',
  userId: 'user-123',
  includeDeleted: false,
  version: 5 // Optional specific version
});
softDeleteContent(contentId, userId, reason?)

Soft delete content.

const success = await service.softDeleteContent('content-1', 'user-123', 'Content no longer needed');
restoreContent(contentId, userId, reason?)

Restore soft deleted content.

const success = await service.restoreContent('content-1', 'user-123', 'Content restored');
getEditorState(sessionId)

Get current editor state for a session.

const state = service.getEditorState(sessionId);
console.log('Current content:', state.contentData);
console.log('Auto-save enabled:', state.autoSaveState.enabled);
console.log('Has unsaved changes:', state.autoSaveState.hasUnsavedChanges);

Event System

Listen to editor service events.

service.onEvent((event) => {
  switch (event.type) {
    case 'session_started':
      console.log('Editor session started:', event.data.sessionId);
      break;
    case 'content_saved':
      console.log('Content saved:', event.contentId);
      break;
    case 'content_auto_saved':
      console.log('Content auto-saved:', event.contentId);
      break;
    case 'content_deleted':
      console.log('Content deleted:', event.contentId);
      break;
    case 'version_created':
      console.log('Version created:', event.data.version);
      break;
  }
});

Hooks System

Add custom processing for editor events.

service.addHook({
  name: 'audit-log',
  hook: async (event) => {
    // Log editor events for audit
    auditLogger.log(event.type, {
      timestamp: event.timestamp,
      contentId: event.contentId,
      userId: event.userId,
      data: event.data
    });
  },
  priority: 1,
  enabled: true,
  eventTypes: ['content_saved', 'content_deleted', 'version_created']
});

Examples

Basic Content Editor

import { ContentEditorService } from '@bernierllc/content-editor-service';

class ContentEditor {
  private service: ContentEditorService;
  private currentSession: string | null = null;

  constructor() {
    this.service = new ContentEditorService({
      enabled: true,
      autoSave: {
        enabled: true,
        debounceMs: 1000,
        storageMode: 'both'
      },
      softDelete: {
        enabled: true,
        showDeletedToUsers: false
      },
      versionHistory: {
        enabled: true,
        maxVersions: 50
      }
    });

    // Set up event handling
    this.service.onEvent((event) => {
      this.handleEditorEvent(event);
    });
  }

  async initialize() {
    await this.service.initialize();
  }

  async openContent(contentId: string, contentTypeId: string, userId: string) {
    // Start editor session
    const editorState = await this.service.startEditorSession({
      contentId,
      contentTypeId,
      userId,
      userPermissions: await this.getUserPermissions(userId),
      contentMetadata: await this.getContentMetadata(contentId)
    });

    this.currentSession = editorState.session.id;

    // Load content
    const loadResult = await this.service.loadContent({
      contentId,
      contentTypeId,
      userId
    });

    if (loadResult.success) {
      return {
        contentData: loadResult.contentData,
        contentType: loadResult.contentType,
        editorState
      };
    } else {
      throw new Error(`Failed to load content: ${loadResult.error}`);
    }
  }

  async saveContent(contentData: any, saveType: 'draft' | 'publish' = 'draft') {
    if (!this.currentSession) {
      throw new Error('No active editor session');
    }

    const editorState = this.service.getEditorState(this.currentSession);
    if (!editorState) {
      throw new Error('Editor session not found');
    }

    const result = await this.service.saveContent({
      contentId: editorState.contentId,
      contentTypeId: editorState.contentTypeId,
      contentData,
      saveType,
      userId: editorState.session.userId,
      reason: `Saving as ${saveType}`,
      createVersion: saveType === 'publish'
    });

    if (result.success) {
      // Update UI to show saved status
      this.updateSaveStatus('saved');
    } else {
      // Show error to user
      this.showError(`Save failed: ${result.error}`);
    }

    return result;
  }

  async closeContent() {
    if (this.currentSession) {
      await this.service.endEditorSession(this.currentSession);
      this.currentSession = null;
    }
  }

  private handleEditorEvent(event: any) {
    switch (event.type) {
      case 'content_auto_saved':
        this.updateSaveStatus('auto-saved');
        break;
      case 'content_saved':
        this.updateSaveStatus('saved');
        break;
      case 'session_ended':
        this.currentSession = null;
        break;
    }
  }

  private updateSaveStatus(status: string) {
    // Update UI to show save status
    console.log(`Save status: ${status}`);
  }

  private showError(message: string) {
    // Show error to user
    console.error(message);
  }
}

Advanced Editor with Validation

class ValidatedContentEditor extends ContentEditor {
  constructor() {
    super();
    
    // Enable validation
    this.service.updateConfig({
      validation: {
        enabled: true,
        validateOnAutoSave: false // Only validate on manual saves
      }
    });
  }

  async saveContent(contentData: any, saveType: 'draft' | 'publish' = 'draft') {
    if (!this.currentSession) {
      throw new Error('No active editor session');
    }

    const editorState = this.service.getEditorState(this.currentSession);
    if (!editorState) {
      throw new Error('Editor session not found');
    }

    // Validate content before saving
    const validation = await this.validateContent(contentData, editorState.contentTypeId);
    if (!validation.valid) {
      return {
        success: false,
        error: `Validation failed: ${validation.errors.join(', ')}`,
        validationErrors: validation.errors
      };
    }

    // Proceed with save
    return super.saveContent(validation.validatedData || contentData, saveType);
  }

  private async validateContent(contentData: any, contentTypeId: string) {
    // Custom validation logic
    const errors: string[] = [];
    const warnings: string[] = [];

    // Check required fields
    if (!contentData.data.title) {
      errors.push('Title is required');
    }

    if (!contentData.data.content) {
      errors.push('Content is required');
    }

    // Check content length
    if (contentData.data.content && contentData.data.content.length < 100) {
      warnings.push('Content is quite short');
    }

    return {
      valid: errors.length === 0,
      errors,
      warnings,
      validatedData: errors.length === 0 ? contentData : undefined
    };
  }
}

Editor with Soft Delete Management

class SoftDeleteAwareEditor extends ContentEditor {
  async deleteContent(contentId: string, userId: string, reason: string) {
    const success = await this.service.softDeleteContent(contentId, userId, reason);
    
    if (success) {
      // Update UI to show deleted status
      this.updateContentStatus(contentId, 'deleted');
      
      // Show confirmation to user
      this.showMessage('Content has been deleted');
    } else {
      this.showError('Failed to delete content');
    }

    return success;
  }

  async restoreContent(contentId: string, userId: string, reason: string) {
    const success = await this.service.restoreContent(contentId, userId, reason);
    
    if (success) {
      // Update UI to show restored status
      this.updateContentStatus(contentId, 'active');
      
      // Show confirmation to user
      this.showMessage('Content has been restored');
    } else {
      this.showError('Failed to restore content');
    }

    return success;
  }

  async loadContentWithDeleted(contentId: string, contentTypeId: string, userId: string) {
    const loadResult = await this.service.loadContent({
      contentId,
      contentTypeId,
      userId,
      includeDeleted: true // Include soft deleted content
    });

    if (loadResult.success && loadResult.softDeleteState?.isDeleted) {
      // Show warning that content is deleted
      this.showWarning('This content has been deleted');
    }

    return loadResult;
  }
}

Editor with Version History

class VersionAwareEditor extends ContentEditor {
  async getVersionHistory(contentId: string) {
    // In a real implementation, this would fetch from a version store
    // For now, return mock data
    return [
      {
        version: 1,
        timestamp: new Date('2023-01-01'),
        userId: 'user-1',
        description: 'Initial version',
        contentData: { /* ... */ }
      },
      {
        version: 2,
        timestamp: new Date('2023-01-02'),
        userId: 'user-1',
        description: 'Added introduction',
        contentData: { /* ... */ }
      }
    ];
  }

  async loadVersion(contentId: string, version: number) {
    const loadResult = await this.service.loadContent({
      contentId,
      contentTypeId: 'text', // Would need to get this from somewhere
      userId: 'current-user',
      version
    });

    if (loadResult.success) {
      // Show warning that this is a historical version
      this.showWarning(`Loading version ${version} from ${loadResult.version?.timestamp}`);
    }

    return loadResult;
  }

  async createVersion(contentId: string, userId: string, description: string) {
    const editorState = this.service.getEditorState(this.currentSession!);
    if (!editorState) {
      throw new Error('No active editor session');
    }

    const result = await this.service.saveContent({
      contentId,
      contentTypeId: editorState.contentTypeId,
      contentData: editorState.contentData,
      saveType: 'draft',
      userId,
      reason: description,
      createVersion: true
    });

    if (result.success) {
      this.showMessage(`Version ${result.version} created: ${description}`);
    }

    return result;
  }
}

Editor Statistics and Analytics

class AnalyticsEnabledEditor extends ContentEditor {
  getEditorStatistics() {
    const stats = this.service.getStatistics();
    
    return {
      totalContent: stats.totalContent,
      activeSessions: stats.activeSessions,
      contentByType: stats.contentByType,
      autoSaveStats: {
        totalAutoSaves: stats.autoSaveStats.totalAutoSaves,
        successRate: stats.autoSaveStats.successfulAutoSaves / stats.autoSaveStats.totalAutoSaves,
        averageTime: stats.autoSaveStats.averageAutoSaveTime
      },
      versionStats: {
        totalVersions: stats.versionStats.totalVersions,
        averageVersionsPerContent: stats.versionStats.averageVersionsPerContent
      },
      softDeleteStats: {
        totalDeleted: stats.softDeleteStats.totalSoftDeleted,
        totalRestored: stats.softDeleteStats.totalRestored
      }
    };
  }

  getContentTypeUsage() {
    const stats = this.service.getStatistics();
    
    return Object.entries(stats.contentByType).map(([typeId, count]) => ({
      typeId,
      count,
      percentage: (count / stats.totalContent) * 100
    }));
  }

  getAutoSavePerformance() {
    const stats = this.service.getStatistics();
    
    return {
      totalAutoSaves: stats.autoSaveStats.totalAutoSaves,
      successfulAutoSaves: stats.autoSaveStats.successfulAutoSaves,
      failedAutoSaves: stats.autoSaveStats.failedAutoSaves,
      successRate: stats.autoSaveStats.successfulAutoSaves / stats.autoSaveStats.totalAutoSaves,
      averageTime: stats.autoSaveStats.averageAutoSaveTime
    };
  }
}

TypeScript Support

This package is written in TypeScript and provides full type definitions. All interfaces and types are exported for use in your own code.

License

UNLICENSED - See LICENSE file for details.

Contributing

Contributions are welcome! Please see the contributing guidelines for more information.

Support

For support and questions, please open an issue on GitHub.