Skip to content

Best Practices ​

Status: this page describes the designed sandboxed extension API, which does not run today. Only native extensions run. See what actually runs.

This guide covers patterns, recommendations, and best practices for building high-quality BluePLM extensions.

Architecture Patterns ​

1. Use the Disposable Pattern ​

Always push subscriptions to context.subscriptions for automatic cleanup:

typescript
// ✓ Good: Resources are auto-disposed
export async function activate(context, api) {
  context.subscriptions.push(
    api.commands.registerCommand('myext.cmd', handler),
    api.workspace.onFileChanged(fileHandler),
    api.events.on('vault:changed', vaultHandler)
  )
}

// ❌ Bad: Memory leak on deactivate
export async function activate(context, api) {
  api.commands.registerCommand('myext.cmd', handler)
  // No cleanup!
}

2. Lazy Initialization ​

Don't do heavy work in activate. Use activation events:

typescript
// ✓ Good: Fast activation, lazy loading
export async function activate(context, api) {
  // Register commands immediately (fast)
  context.subscriptions.push(
    api.commands.registerCommand('myext.sync', async () => {
      // Heavy work only when command is invoked
      const data = await loadLargeDataset()
      await performSync(data)
    })
  )
}

// ❌ Bad: Slow activation
export async function activate(context, api) {
  // This blocks activation
  const data = await loadLargeDataset()
  
  context.subscriptions.push(
    api.commands.registerCommand('myext.sync', () => performSync(data))
  )
}

3. Activation Events ​

Choose specific activation events for fast startup:

json
// ✓ Good: Specific events
"activationEvents": [
  "onCommand:myext.sync",
  "onNavigate:settings/extensions/myext"
]

// ❌ Avoid: Loads on every startup
"activationEvents": [
  "onStartup"
]

Only use onStartup if:

  • Extension needs to run background tasks always
  • Extension intercepts global events

4. Separate Concerns ​

Organize code by responsibility:

client/
├── index.ts              # Entry point only
├── commands/             # Command handlers
│   ├── sync.ts
│   └── configure.ts
├── views/                # React components
│   ├── Panel.tsx
│   └── Settings.tsx
├── services/             # Business logic
│   ├── syncService.ts
│   └── apiClient.ts
└── utils/                # Pure utilities
    ├── formatting.ts
    └── validation.ts

Error Handling ​

1. Always Handle Async Errors ​

typescript
// ✓ Good: Errors handled
context.subscriptions.push(
  api.commands.registerCommand('myext.sync', async () => {
    try {
      await performSync(api)
    } catch (error) {
      context.log.error('Sync failed:', error)
      api.ui.showToast('Sync failed. Check logs for details.', 'error')
      api.telemetry.trackError(error as Error, { operation: 'sync' })
    }
  })
)

// ❌ Bad: Unhandled rejection
api.commands.registerCommand('myext.sync', async () => {
  await performSync(api) // Might throw!
})

2. User-Friendly Error Messages ​

typescript
// ✓ Good: Helpful message
try {
  await api.callOrgApi('/extensions/myext/sync', { ... })
} catch (error) {
  if (error.message.includes('401')) {
    api.ui.showToast('Session expired. Please log in again.', 'error')
  } else if (error.message.includes('timeout')) {
    api.ui.showToast('Server is slow. Please try again.', 'warning')
  } else {
    api.ui.showToast('Sync failed. Check your connection.', 'error')
  }
}

// ❌ Bad: Exposing technical details
api.ui.showToast(`Error: ${error.stack}`, 'error')

3. Graceful Degradation ​

typescript
async function getStatus(api: ExtensionClientAPI) {
  try {
    const response = await api.callOrgApi('/extensions/myext/status')
    return response.data
  } catch (error) {
    // Fallback to cached status
    const cached = await api.storage.get('lastStatus')
    if (cached) {
      context.log.warn('Using cached status due to API error')
      return { ...cached, stale: true }
    }
    throw error
  }
}

Performance ​

1. Startup Time Budget ​

Extensions should activate in under 200ms:

typescript
export async function activate(context, api) {
  const start = performance.now()
  
  // Register handlers (fast)
  registerCommands(context, api)
  registerEventListeners(context, api)
  
  const duration = performance.now() - start
  if (duration > 200) {
    context.log.warn(`Slow activation: ${duration}ms`)
  }
}

2. Debounce Frequent Events ​

typescript
import { debounce } from './utils'

const debouncedSync = debounce(async (events: FileChangeEvent[]) => {
  await performIncrementalSync(events)
}, 500)

context.subscriptions.push(
  api.workspace.onFileChanged(debouncedSync)
)

Simple debounce implementation:

typescript
function debounce<T extends (...args: any[]) => any>(
  fn: T,
  ms: number
): (...args: Parameters<T>) => void {
  let timeout: ReturnType<typeof setTimeout> | undefined
  return (...args) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => fn(...args), ms)
  }
}

3. Cache Expensive Operations ​

typescript
let cachedVaults: VaultInfo[] | undefined

async function getVaults(api: ExtensionClientAPI): Promise<VaultInfo[]> {
  if (cachedVaults) return cachedVaults
  
  cachedVaults = await api.workspace.getVaults()
  return cachedVaults
}

// Invalidate on vault changes
context.subscriptions.push(
  api.events.on('vault:changed', () => {
    cachedVaults = undefined
  })
)

4. Batch Operations ​

typescript
// ✓ Good: Batch storage operations
const settings = {
  autoSync: true,
  interval: 300,
  excludePatterns: ['*.tmp']
}
await api.storage.set('settings', settings)

// ❌ Bad: Multiple round-trips
await api.storage.set('autoSync', true)
await api.storage.set('interval', 300)
await api.storage.set('excludePatterns', ['*.tmp'])

UI/UX ​

1. Provide Clear Feedback ​

typescript
async function handleSync(api: ExtensionClientAPI) {
  // Show progress for long operations
  await api.ui.showProgress(
    { title: 'Syncing files...', cancellable: true },
    async (progress, token) => {
      progress.report({ message: 'Preparing...' })
      
      const files = await getFiles()
      for (let i = 0; i < files.length; i++) {
        if (token.isCancellationRequested) {
          api.ui.showToast('Sync cancelled', 'info')
          return
        }
        
        progress.report({
          message: `Syncing ${files[i].name}`,
          increment: 100 / files.length
        })
        
        await syncFile(files[i])
      }
      
      api.ui.showToast('Sync complete!', 'success')
    }
  )
}

2. Confirm Destructive Actions ​

typescript
async function handleDelete(api: ExtensionClientAPI, file: string) {
  const result = await api.ui.showDialog({
    title: 'Delete File',
    message: `Are you sure you want to delete "${file}"? This cannot be undone.`,
    type: 'confirm',
    confirmText: 'Delete',
    cancelText: 'Cancel'
  })
  
  if (result.confirmed) {
    await deleteFile(file)
    api.ui.showToast('File deleted', 'success')
  }
}

3. Use Status Indicators ​

typescript
// Show connection status
async function checkConnection(api: ExtensionClientAPI) {
  api.ui.setStatus('checking')
  
  try {
    await api.callOrgApi('/extensions/myext/ping')
    api.ui.setStatus('online')
  } catch {
    api.ui.setStatus('offline')
  }
}

// Update on reconnection
context.subscriptions.push(
  api.events.on('online:changed', (isOnline) => {
    api.ui.setStatus(isOnline ? 'online' : 'offline')
  })
)

4. Respect User Preferences ​

typescript
async function maybeShowHint(api: ExtensionClientAPI) {
  const dismissed = await api.storage.get<boolean>('hintDismissed')
  
  if (!dismissed) {
    const result = await api.ui.showDialog({
      title: 'Tip',
      message: 'Press Ctrl+Shift+S to sync quickly!',
      type: 'info',
      confirmText: 'Got it',
      cancelText: "Don't show again"
    })
    
    if (!result.confirmed && !result.dismissed) {
      await api.storage.set('hintDismissed', true)
    }
  }
}

Security ​

1. Validate All Input ​

typescript
// Server handler
export default async function handler(api: ExtensionServerAPI) {
  const { vaultId, action } = api.request.body as {
    vaultId?: unknown
    action?: unknown
  }
  
  // Type and format validation
  if (typeof vaultId !== 'string' || !vaultId.match(/^[a-f0-9-]{36}$/)) {
    return api.response.error('Invalid vaultId', 400)
  }
  
  if (!['sync', 'backup', 'restore'].includes(String(action))) {
    return api.response.error('Invalid action', 400)
  }
  
  // Safe to use
  await performAction(vaultId, action as 'sync' | 'backup' | 'restore')
}

2. Never Expose Secrets ​

typescript
// ❌ Bad: Leaking secrets
return api.response.json({
  apiKey: await api.secrets.get('api_key')
})

// ✓ Good: Only expose status
return api.response.json({
  configured: !!(await api.secrets.get('api_key'))
})

3. Use HTTPS for External APIs ​

json
// ✓ Good: HTTPS only
"permissions": {
  "server": ["http:domain:api.example.com"]
}

// In handler
await api.http.fetch('https://api.example.com/data')

4. Check Permissions Before Actions ​

typescript
export default async function handler(api: ExtensionServerAPI) {
  const { user } = api
  
  // Ensure authenticated
  if (!user) {
    return api.response.error('Authentication required', 401)
  }
  
  // Check role for admin actions
  if (api.request.body.action === 'deleteAll') {
    if (user.role !== 'admin' && user.role !== 'owner') {
      return api.response.error('Admin access required', 403)
    }
  }
}

Testing ​

1. Mock the API ​

typescript
// test/commands.test.ts
const mockApi: Partial<ExtensionClientAPI> = {
  ui: {
    showToast: jest.fn(),
    showDialog: jest.fn().mockResolvedValue({ confirmed: true }),
    showProgress: jest.fn((opts, task) => task(
      { report: jest.fn() },
      { isCancellationRequested: false }
    )),
    setStatus: jest.fn(),
    showQuickPick: jest.fn(),
    showInputBox: jest.fn()
  },
  storage: {
    get: jest.fn(),
    set: jest.fn(),
    delete: jest.fn(),
    keys: jest.fn(),
    has: jest.fn(),
    clear: jest.fn()
  },
  callOrgApi: jest.fn().mockResolvedValue({
    ok: true,
    data: { fileCount: 10 }
  })
}

test('sync command shows success toast', async () => {
  await syncCommand(mockApi as ExtensionClientAPI)
  
  expect(mockApi.ui?.showToast).toHaveBeenCalledWith(
    expect.stringContaining('complete'),
    'success'
  )
})

2. Test Edge Cases ​

typescript
test('handles API timeout gracefully', async () => {
  mockApi.callOrgApi = jest.fn().mockRejectedValue(new Error('timeout'))
  
  await syncCommand(mockApi as ExtensionClientAPI)
  
  expect(mockApi.ui?.showToast).toHaveBeenCalledWith(
    expect.stringContaining('try again'),
    'error'
  )
})

test('handles no vault selected', async () => {
  mockApi.workspace = {
    ...mockApi.workspace,
    getCurrentVault: jest.fn().mockResolvedValue(undefined)
  }
  
  await syncCommand(mockApi as ExtensionClientAPI)
  
  expect(mockApi.ui?.showToast).toHaveBeenCalledWith(
    'No vault selected',
    'error'
  )
})

3. Test Settings Persistence ​

typescript
test('settings are persisted', async () => {
  const storage = new Map()
  mockApi.storage = {
    set: jest.fn((key, value) => { storage.set(key, value); return Promise.resolve() }),
    get: jest.fn((key) => Promise.resolve(storage.get(key))),
    // ...
  }
  
  await saveSettings(mockApi, { autoSync: true, interval: 300 })
  const loaded = await loadSettings(mockApi)
  
  expect(loaded).toEqual({ autoSync: true, interval: 300 })
})

Code Organization ​

1. Type Everything ​

typescript
// types.ts
export interface SyncOptions {
  vaultId: string
  force?: boolean
  dryRun?: boolean
}

export interface SyncResult {
  fileCount: number
  duration: number
  errors: string[]
}

// sync.ts
async function performSync(
  api: ExtensionClientAPI,
  options: SyncOptions
): Promise<SyncResult> {
  // Type-safe implementation
}

2. Use Constants ​

typescript
// constants.ts
export const STORAGE_KEYS = {
  SETTINGS: 'settings',
  LAST_SYNC: 'lastSync',
  CACHE: 'cache'
} as const

export const COMMANDS = {
  SYNC: 'myext.sync',
  CONFIGURE: 'myext.configure'
} as const

// Usage
await api.storage.set(STORAGE_KEYS.SETTINGS, settings)
api.commands.registerCommand(COMMANDS.SYNC, handler)

3. Document Complex Logic ​

typescript
/**
 * Performs incremental sync by comparing local and remote file hashes.
 * 
 * Algorithm:
 * 1. Get list of local files with hashes
 * 2. Fetch remote file list from API
 * 3. Compare hashes to find changed files
 * 4. Upload changed local files
 * 5. Download changed remote files
 * 6. Handle conflicts based on user preference
 * 
 * @param api - Extension client API
 * @param vaultId - Target vault ID
 * @returns Sync result with counts and any errors
 */
async function performIncrementalSync(
  api: ExtensionClientAPI,
  vaultId: string
): Promise<SyncResult> {
  // Implementation...
}

Common Patterns ​

Configuration Loading ​

typescript
interface ExtensionConfig {
  autoSync: boolean
  syncInterval: number
  excludePatterns: string[]
}

const DEFAULT_CONFIG: ExtensionConfig = {
  autoSync: true,
  syncInterval: 300,
  excludePatterns: ['*.tmp', '.git/**']
}

async function loadConfig(api: ExtensionClientAPI): Promise<ExtensionConfig> {
  const saved = await api.storage.get<Partial<ExtensionConfig>>('config')
  return { ...DEFAULT_CONFIG, ...saved }
}

async function saveConfig(
  api: ExtensionClientAPI,
  config: ExtensionConfig
): Promise<void> {
  await api.storage.set('config', config)
  api.events.emit('myext.configChanged', config)
}

Periodic Background Tasks ​

typescript
let intervalId: ReturnType<typeof setInterval> | undefined

export async function activate(context, api) {
  const config = await loadConfig(api)
  
  if (config.autoSync) {
    intervalId = setInterval(async () => {
      try {
        await performBackgroundSync(api)
      } catch (error) {
        context.log.error('Background sync failed:', error)
      }
    }, config.syncInterval * 1000)
  }
  
  // Clean up on deactivate
  context.subscriptions.push({
    dispose: () => {
      if (intervalId) clearInterval(intervalId)
    }
  })
}

State Machine for Complex Operations ​

typescript
type SyncState = 'idle' | 'syncing' | 'error' | 'paused'

class SyncManager {
  private state: SyncState = 'idle'
  private listeners: Set<(state: SyncState) => void> = new Set()
  
  async sync(api: ExtensionClientAPI): Promise<void> {
    if (this.state === 'syncing') return
    
    this.setState('syncing')
    
    try {
      await performSync(api)
      this.setState('idle')
    } catch (error) {
      this.setState('error')
      throw error
    }
  }
  
  private setState(state: SyncState) {
    this.state = state
    this.listeners.forEach(l => l(state))
  }
  
  onStateChange(callback: (state: SyncState) => void): () => void {
    this.listeners.add(callback)
    return () => this.listeners.delete(callback)
  }
}

← Publishing Guide | Troubleshooting →