Data Provider Plugins
Integrate external data sources and services into Lokus. Create connections to APIs, databases, and cloud services.
What are Data Providers?
Data providers allow you to:
- Fetch data from external APIs
- Sync with cloud services (Jira, Trello, GitHub, etc.)
- Query databases
- Display external data in Lokus
- Real-time data synchronization
Basic Data Provider
import { Plugin, PluginContext } from '@lokus/plugin-sdk'
export default class DataProviderPlugin implements Plugin {
private api!: LokusAPI
private cache = new Map()
async activate(context: PluginContext) {
this.api = context.api
// Register data provider
context.api.registerDataProvider('myapi', this)
// Register commands
this.registerCommands(context)
// Start sync
this.startSync()
}
// Fetch data from external API
async fetchData(query: string): Promise<any[]> {
try {
const response = await this.api.network.fetch(
`https://api.example.com/data?q=${encodeURIComponent(query)}`
)
return await response.json()
} catch (error) {
this.api.log('error', 'Failed to fetch data', error)
return []
}
}
// Sync data periodically
private startSync() {
setInterval(() => this.syncData(), 60000) // Every minute
}
private async syncData() {
const data = await this.fetchData('*')
await this.api.storage.set('syncedData', data)
this.api.events.emit('myPlugin.dataUpdated', data)
}
}
GitHub Integration Example
class GitHubProviderPlugin implements Plugin {
private api!: LokusAPI
private token?: string
async activate(context: PluginContext) {
this.api = context.api
// Get stored token
this.token = await context.secrets.get('github-token')
if (!this.token) {
await this.authenticateUser()
}
// Register commands
context.subscriptions.push(
context.api.commands.register({
id: 'github.fetchIssues',
title: 'Fetch GitHub Issues',
handler: () => this.fetchIssues()
})
)
// Create tree view for issues
this.createIssuesView(context)
}
private async authenticateUser() {
const token = await this.api.ui.showInputBox({
prompt: 'Enter GitHub Personal Access Token',
password: true,
validateInput: (value) => {
if (!value) return 'Token is required'
if (value.length < 20) return 'Invalid token format'
return null
}
})
if (token) {
await this.context.secrets.store('github-token', token)
this.token = token
}
}
private async fetchIssues(repo?: string) {
if (!this.token) {
await this.authenticateUser()
return
}
try {
const response = await this.api.network.fetch(
`https://api.github.com/repos/${repo || 'user/repo'}/issues`,
{
headers: {
'Authorization': `Bearer ${this.token}`,
'Accept': 'application/vnd.github.v3+json'
}
}
)
const issues = await response.json()
// Display in panel
this.displayIssues(issues)
// Cache for offline access
await this.api.storage.set('github-issues', issues)
return issues
} catch (error) {
this.api.ui.showNotification(
'Failed to fetch GitHub issues',
'error'
)
throw error
}
}
private displayIssues(issues: any[]) {
const html = `
<!DOCTYPE html>
<html>
<body>
<h2>GitHub Issues</h2>
<ul>
${issues.map(issue => `
<li>
<strong>#${issue.number}</strong>: ${issue.title}
<br>
<small>${issue.state} - ${issue.user.login}</small>
</li>
`).join('')}
</ul>
</body>
</html>
`
// Update panel
this.panel.webview.html = html
}
private createIssuesView(context: PluginContext) {
this.panel = context.api.ui.registerWebviewPanel({
id: 'github.issues',
title: 'GitHub Issues',
type: 'webview',
location: 'sidebar',
icon: 'github'
})
context.subscriptions.push(this.panel)
}
}
REST API Integration
class RESTProviderPlugin implements Plugin {
private baseUrl = 'https://api.example.com'
private apiKey?: string
async activate(context: PluginContext) {
this.apiKey = await context.api.config.get('myPlugin.apiKey')
// CRUD operations
context.subscriptions.push(
context.api.commands.register({
id: 'myPlugin.createItem',
title: 'Create Item',
handler: () => this.createItem()
}),
context.api.commands.register({
id: 'myPlugin.listItems',
title: 'List Items',
handler: () => this.listItems()
})
)
}
private async request(endpoint: string, options: RequestInit = {}) {
const url = `${this.baseUrl}${endpoint}`
const response = await this.api.network.fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
...options.headers
}
})
if (!response.ok) {
throw new Error(`API request failed: ${response.statusText}`)
}
return response.json()
}
async listItems() {
try {
const items = await this.request('/items')
return items
} catch (error) {
this.api.ui.showNotification('Failed to fetch items', 'error')
throw error
}
}
async createItem(data: any) {
try {
const item = await this.request('/items', {
method: 'POST',
body: JSON.stringify(data)
})
return item
} catch (error) {
this.api.ui.showNotification('Failed to create item', 'error')
throw error
}
}
async updateItem(id: string, data: any) {
return this.request(`/items/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
})
}
async deleteItem(id: string) {
return this.request(`/items/${id}`, {
method: 'DELETE'
})
}
}
Caching and Offline Support
class CachedProviderPlugin implements Plugin {
private cache = new Map<string, CacheEntry>()
private cacheTTL = 5 * 60 * 1000 // 5 minutes
async fetchWithCache<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
// Check cache
const cached = this.cache.get(key)
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
return cached.data as T
}
// Fetch fresh data
try {
const data = await fetcher()
// Update cache
this.cache.set(key, {
data,
timestamp: Date.now()
})
// Persist to storage
await this.api.storage.set(`cache:${key}`, {
data,
timestamp: Date.now()
})
return data
} catch (error) {
// Return stale cache on error
if (cached) {
this.api.ui.showNotification(
'Using cached data (network error)',
'warning'
)
return cached.data as T
}
throw error
}
}
async getData(id: string) {
return this.fetchWithCache(`data:${id}`, () =>
this.api.network.fetch(`https://api.example.com/data/${id}`)
.then(r => r.json())
)
}
}
interface CacheEntry {
data: any
timestamp: number
}
Real-time Data Sync
class RealtimeProviderPlugin implements Plugin {
private ws?: WebSocket
private reconnectTimeout?: NodeJS.Timeout
async activate(context: PluginContext) {
this.connectWebSocket()
context.subscriptions.push({
dispose: () => {
this.ws?.close()
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout)
}
}
})
}
private connectWebSocket() {
this.ws = new WebSocket('wss://api.example.com/realtime')
this.ws.onopen = () => {
console.log('WebSocket connected')
this.api.ui.showNotification('Connected to server', 'success')
// Subscribe to channels
this.ws!.send(JSON.stringify({
type: 'subscribe',
channels: ['updates', 'notifications']
}))
}
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data)
this.handleRealtimeMessage(message)
}
this.ws.onerror = (error) => {
console.error('WebSocket error:', error)
}
this.ws.onclose = () => {
console.log('WebSocket closed, reconnecting...')
// Reconnect after delay
this.reconnectTimeout = setTimeout(() => {
this.connectWebSocket()
}, 5000)
}
}
private handleRealtimeMessage(message: any) {
switch (message.type) {
case 'update':
this.api.events.emit('dataUpdated', message.data)
break
case 'notification':
this.api.ui.showNotification(message.text, message.level)
break
}
}
}
Authentication
class AuthProviderPlugin implements Plugin {
async authenticate(): Promise<string> {
// OAuth flow
const authUrl = await this.getAuthUrl()
// Open browser
await this.api.ui.openExternal(authUrl)
// Wait for callback
const token = await this.waitForCallback()
// Store token
await this.context.secrets.store('auth-token', token)
return token
}
private async getAuthUrl(): Promise<string> {
const clientId = 'your-client-id'
const redirectUri = 'lokus://auth-callback'
const scopes = 'read write'
return `https://oauth.example.com/authorize?` +
`client_id=${clientId}&` +
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
`scope=${encodeURIComponent(scopes)}&` +
`response_type=code`
}
private async waitForCallback(): Promise<string> {
return new Promise((resolve, reject) => {
// Register URI handler
const disposable = this.api.registerUriHandler((uri) => {
if (uri.startsWith('lokus://auth-callback')) {
const url = new URL(uri)
const code = url.searchParams.get('code')
if (code) {
this.exchangeCodeForToken(code).then(resolve)
} else {
reject(new Error('No auth code received'))
}
disposable.dispose()
}
})
// Timeout after 5 minutes
setTimeout(() => {
disposable.dispose()
reject(new Error('Authentication timeout'))
}, 5 * 60 * 1000)
})
}
private async exchangeCodeForToken(code: string): Promise<string> {
const response = await this.api.network.fetch(
'https://oauth.example.com/token',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
client_id: 'your-client-id',
client_secret: 'your-client-secret',
grant_type: 'authorization_code'
})
}
)
const data = await response.json()
return data.access_token
}
}
Best Practices
Error Handling
async function safeFetch<T>(
fetcher: () => Promise<T>,
fallback: T
): Promise<T> {
try {
return await fetcher()
} catch (error) {
console.error('Fetch failed:', error)
// Show user-friendly error
api.ui.showNotification(
'Failed to load data. Using cached version.',
'warning'
)
return fallback
}
}
Rate Limiting
class RateLimiter {
private queue: Array<() => Promise<any>> = []
private processing = false
private requestsPerMinute = 60
async enqueue<T>(request: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
const result = await request()
resolve(result)
} catch (error) {
reject(error)
}
})
this.processQueue()
})
}
private async processQueue() {
if (this.processing || this.queue.length === 0) return
this.processing = true
while (this.queue.length > 0) {
const request = this.queue.shift()!
await request()
// Wait between requests
await new Promise(resolve =>
setTimeout(resolve, 60000 / this.requestsPerMinute)
)
}
this.processing = false
}
}