DeesseJS

Extensions Overview

Understanding the extension system in DeesseJS

Extensions Overview

Extensions are predefined interfaces that providers must implement to offer common functionality like caching, jobs, logging, etc. They provide a unified API while allowing different provider implementations.

Extensions vs Plugins

AspectExtensionsPlugins
PurposeCommon infrastructure featuresApplication-specific features
InterfacePredefined by DeesseJSDefined by plugin author
ProvidersMultiple providers availableSingle implementation
Use casesCache, jobs, logs, queuesSEO, analytics, custom fields
DependenciesPlugins can depend on extensionsExtensions don't depend on plugins

How Extensions Work

The Extension Interface

DeesseJS defines the interface:

// Extension interface defined by DeesseJS
interface CacheExtension {
  get<T>(key: string): Promise<T | null>
  set<T>(key: string, value: T, ttl?: number): Promise<void>
  delete(key: string): Promise<void>
  clear(): Promise<void>
}

Provider Implementations

Different providers implement the same interface:

// Redis provider
import { Redis } from 'ioredis'

export class RedisCacheProvider implements CacheExtension {
  private client: Redis

  constructor(client: Redis) {
    this.client = client
  }

  async get<T>(key: string): Promise<T | null> {
    const value = await this.client.get(key)
    return value ? JSON.parse(value) : null
  }

  async set<T>(key: string, value: T, ttl?: number): Promise<void> {
    const serialized = JSON.stringify(value)
    if (ttl) {
      await this.client.setex(key, ttl, serialized)
    } else {
      await this.client.set(key, serialized)
    }
  }

  async delete(key: string): Promise<void> {
    await this.client.del(key)
  }

  async clear(): Promise<void> {
    await this.client.flushdb()
  }
}
// Memory provider
export class MemoryCacheProvider implements CacheExtension {
  private cache = new Map<string, { value: any; expires?: number }>()

  async get<T>(key: string): Promise<T | null> {
    const item = this.cache.get(key)
    if (!item) return null

    if (item.expires && Date.now() > item.expires) {
      this.cache.delete(key)
      return null
    }

    return item.value as T
  }

  async set<T>(key: string, value: T, ttl?: number): Promise<void> {
    this.cache.set(key, {
      value,
      expires: ttl ? Date.now() + ttl * 1000 : undefined,
    })
  }

  async delete(key: string): Promise<void> {
    this.cache.delete(key)
  }

  async clear(): Promise<void> {
    this.cache.clear()
  }
}

Configuration

Configure which provider to use:

// deesse.config.ts
import { defineConfig } from '@deessejs/core'
import { RedisCacheProvider } from '@deessejs/extensions-redis'
import { Redis } from 'ioredis'

const redis = new Redis(process.env.REDIS_URL)

export const config = defineConfig({
  extensions: {
    cache: {
      provider: new RedisCacheProvider(redis),
      options: {
        prefix: 'deesse:',
        defaultTTL: 3600,
      },
    },
  },
})

Available Extensions

Cache Extension

Caching layer with multiple providers.

Providers:

  • @deessejs/extensions-cache-memory - In-memory cache (default)
  • @deessejs/extensions-cache-redis - Redis cache
  • @deessejs/extensions-cache-memcached - Memcached cache
// Usage in your code
import { cache } from '@deessejs/extensions'

await cache.set('user:123', user, 3600)
const user = await cache.get('user:123')
await cache.delete('user:123')

Queue Extension

Job queue for background tasks.

Providers:

  • @deessejs/extensions-queue-bullmq - BullMQ (Redis-based)
  • @deessejs/extensions-queue-bull - Bull (Redis-based)
  • @deessejs/extensions-queue-memory - In-memory queue
import { queue } from '@deessejs/extensions'

await queue.add('send-email', { to: 'user@example.com', subject: 'Hello' })
await queue.add('process-image', { imageId: 'abc123' })

Logger Extension

Structured logging with multiple outputs.

Providers:

  • @deessejs/extensions-logger-pino - Pino logger
  • @deessejs/extensions-logger-winston - Winston logger
  • @deessejs/extensions-logger-console - Console logger (default)
import { logger } from '@deessejs/extensions'

logger.info('User logged in', { userId: '123' })
logger.error('Database connection failed', { error: err.message })
logger.debug('Cache stats', { hits: 100, misses: 5 })

Storage Extension

File storage for uploads and assets.

Providers:

  • @deessejs/extensions-storage-local - Local filesystem
  • @deessejs/extensions-storage-s3 - AWS S3
  • @deessejs/extensions-storage-cloudflare - Cloudflare R2
  • @deessejs/extensions-storage-azure - Azure Blob Storage
import { storage } from '@deessejs/extensions'

const url = await storage.upload('uploads/avatar.jpg', file)
await storage.delete('uploads/avatar.jpg')
const stream = await storage.download('uploads/avatar.jpg')

Search Extension

Full-text search capabilities.

Providers:

  • @deessejs/extensions-search-meilisearch - Meilisearch
  • @deessejs/extensions-search-algolia - Algolia
  • @deessejs/extensions-search-typesense - Typesense
import { search } from '@deessejs/extensions'

await search.index('posts').addDocuments([
  { id: 1, title: 'Hello World', content: '...' }
])

const results = await search.index('posts').search('hello')

Lock Extension

Distributed locking for concurrent operations.

Providers:

  • @deessejs/extensions-lock-redis - Redis-based locks
  • @deessejs/extensions-lock-postgres - PostgreSQL advisory locks
  • @deessejs/extensions-lock-memory - In-memory locks
import { lock } from '@deessejs/extensions'

await lock.acquire('process-payment:123', async () => {
  // Only one execution at a time
  await processPayment(123)
}, { ttl: 30000 })

Events Extension

Event bus for pub/sub messaging.

Providers:

  • @deessejs/extensions-events-redis - Redis pub/sub
  • @deessejs/extensions-events-emitter - Node.js EventEmitter
  • @deessejs/extensions-events-kafka - Apache Kafka
import { events } from '@deessejs/extensions'

// Publish
await events.publish('user.created', { userId: '123', email: 'user@example.com' })

// Subscribe
events.subscribe('user.created', async (data) => {
  await sendWelcomeEmail(data.email)
})

Plugin Dependencies

Plugins can declare required extensions:

// my-plugin/index.ts
export const myPlugin = definePlugin({
  name: 'my-plugin',
  extensions: {
    // Require cache extension
    cache: true,

    // Require logger extension
    logger: true,

    // Optional search extension
    search: false,
  },

  activate(options, { extensions }) {
    // Extensions are available here
    const { cache, logger, search } = extensions

    return {
      hooks: {
        afterCreate: async ({ item }) => {
          // Use cache extension
          await cache.set(`item:${item.id}`, item, 3600)

          // Use logger extension
          logger.info('Item created', { id: item.id })

          // Use search extension if available
          if (search) {
            await search.index('items').addDocuments([item])
          }
        },
      },
    }
  },
})

Extension Availability Check

Check if extensions are available before use:

export const pluginWithOptionalExtension = definePlugin({
  name: 'my-plugin',
  extensions: {
    cache: false, // Optional
    search: false, // Optional
  },

  activate(options, { extensions }) {
    return {
      hooks: {
        afterCreate: async ({ item }) => {
          // Check if cache is available
          if (extensions.cache) {
            await extensions.cache.set(`item:${item.id}`, item)
          }

          // Check if search is available
          if (extensions.search) {
            await extensions.search.index('items').addDocuments([item])
          } else {
            console.warn('Search extension not available')
          }
        },
      },
    }
  },
})

Multiple Providers

You can use multiple providers for the same extension:

export const config = defineConfig({
  extensions: {
    cache: {
      // Primary cache
      default: {
        provider: new RedisCacheProvider(redis),
        options: { prefix: 'cache:' },
      },
      // Session cache
      sessions: {
        provider: new RedisCacheProvider(redis),
        options: { prefix: 'sessions:' },
      },
      // API cache (memory-based for speed)
      api: {
        provider: new MemoryCacheProvider(),
        options: { maxSize: 1000 },
      },
    },
  },
})

Testing with Extensions

Use mock providers during testing:

// tests/setup.ts
import { MockCacheProvider, MockLoggerProvider } from '@deessejs/extensions/test'

export const config = defineConfig({
  extensions: {
    cache: {
      provider: new MockCacheProvider(),
    },
    logger: {
      provider: new MockLoggerProvider(),
    },
  },
})

Performance Considerations

Choose the Right Provider

Use CaseRecommended Provider
DevelopmentMemory providers
Single-server productionMemory or Redis
Multi-server productionRedis, PostgreSQL
High-scaleRedis Cluster, dedicated services
ServerlessExternal services (Redis Cloud, Upstash)

Extension Overhead

Extensions add minimal overhead:

  • Interface abstraction: ~1-2ms
  • Provider initialization: Once at startup
  • Method calls: Depends on provider (Redis: ~1ms, Memory: ~0.1ms)

Best Practices

  1. Use appropriate providers - Memory for dev, Redis for production
  2. Configure TTLs - Set appropriate expiration times
  3. Handle failures gracefully - Cache/queue failures shouldn't break your app
  4. Monitor extension usage - Track cache hit rates, queue lengths
  5. Test with mocks - Use mock providers in tests

Next Steps

On this page