Skip to content

Latest commit

 

History

History
365 lines (304 loc) · 8.46 KB

technical-design.md

File metadata and controls

365 lines (304 loc) · 8.46 KB

コラボレーション機能の技術設計 with Automerge

Automerge採用の理由

  1. 最新のテクノロジースタック

    • Rust/Wasmコアで高性能
    • TypeScriptバインディング
    • メモリ効率の良い実装
  2. 充実した機能セット

    • JSON構造のCRDT
    • テキストCRDT
    • バイナリデータサポート
    • カスタムCRDTの定義
  3. 優れた開発者エクスペリエンス

    • 型安全なAPI
    • 豊富なドキュメント
    • アクティブなコミュニティ

基本実装

フロントエンド実装

import { Automerge } from '@automerge/automerge'
import { AutomergeUrl } from '@automerge/automerge-repo'
import { BroadcastChannelNetworkAdapter } from '@automerge/automerge-repo-network-broadcastchannel'
import { IndexedDBStorageAdapter } from '@automerge/automerge-repo-storage-indexeddb'

// ドキュメントの型定義
interface CollaborativeDoc {
  content: string
  cursor: {
    anchor: number
    head: number
  }
  metadata: {
    title: string
    lastModified: number
  }
}

// Automergeレポジトリの設定
const repo = new Automerge.Repo({
  network: [new BroadcastChannelNetworkAdapter()],
  storage: new IndexedDBStorageAdapter(),
})

// Reactコンポーネント
function CollaborativeEditor() {
  const [doc, changeDoc] = useAutomerge<CollaborativeDoc>(docUrl)
  
  const handleChange = (newContent: string) => {
    changeDoc(doc => {
      doc.content = newContent
      doc.metadata.lastModified = Date.now()
    })
  }

  const handleCursorMove = (anchor: number, head: number) => {
    changeDoc(doc => {
      doc.cursor = { anchor, head }
    })
  }

  return (
    <Editor
      value={doc.content}
      cursor={doc.cursor}
      onChange={handleChange}
      onCursorMove={handleCursorMove}
    />
  )
}

バックエンド実装(Gleam)

import gleam/automerge.{type Doc}
import gleam/json
import gleam/websocket

pub type ServerDoc {
  ServerDoc(
    id: String,
    doc: Doc,
    connections: List(Connection),
  )
}

pub fn handle_connection(state: ServerState, conn: Connection) {
  // WebSocket接続の確立
  let client = ClientInfo(
    id: generate_client_id(),
    connection: conn,
  )
  
  // メッセージハンドラーの設定
  conn
  |> websocket.on_message(fn(msg) {
    handle_sync_message(state, client, msg)
  })
}

fn handle_sync_message(state: ServerState, client: ClientInfo, msg: Message) {
  case msg {
    // Automerge同期メッセージの処理
    SyncMessage(changes) -> {
      let doc = get_document(state, client.doc_id)
      let updated_doc = automerge.apply_changes(doc, changes)
      
      // 変更を他のクライアントに配信
      broadcast_changes(state, client.doc_id, changes)
      
      // 永続化
      persist_document(updated_doc)
    }
    
    // その他のメッセージ処理
    _ -> Ok(Nil)
  }
}

オフライン対応

クライアントサイドの永続化

// IndexedDBストレージアダプターの設定
const storage = new IndexedDBStorageAdapter()

// カスタム永続化ロジック
class CustomStorage extends IndexedDBStorageAdapter {
  async save(docUrl: AutomergeUrl, doc: Automerge.Doc<any>) {
    // 変更をローカルに保存
    await super.save(docUrl, doc)
    
    // メタデータの更新
    await this.saveMetadata(docUrl, {
      lastSaved: Date.now(),
      changeCount: doc.getHistory().length
    })
  }
}

// オフライン検知と再接続
const setupOfflineSupport = (repo: Automerge.Repo) => {
  window.addEventListener('offline', () => {
    repo.networkSubsystem.pause()
  })
  
  window.addEventListener('online', async () => {
    repo.networkSubsystem.resume()
    await repo.sync()
  })
}

変更の同期

// 同期マネージャー
class SyncManager {
  constructor(
    private repo: Automerge.Repo,
    private storage: CustomStorage
  ) {}

  async syncChanges() {
    const pendingDocs = await this.storage.getPendingDocs()
    
    for (const docUrl of pendingDocs) {
      const doc = await this.repo.find(docUrl)
      if (doc) {
        await this.repo.sync(docUrl)
      }
    }
  }

  async handleConflicts(docUrl: AutomergeUrl) {
    const doc = await this.repo.find(docUrl)
    if (doc) {
      // Automergeは自動的にコンフリクトを解決
      // 必要に応じてカスタムの解決ロジックを追加
      const conflicts = doc.getConflicts()
      if (conflicts.length > 0) {
        this.logConflictResolution(conflicts)
      }
    }
  }
}

スケーラビリティ設計

WebSocket同期サーバー

// サーバーサイドの実装
import { Automerge } from '@automerge/automerge'
import { AutomergeServer } from '@automerge/automerge-server'

const server = new AutomergeServer({
  // Redis永続化アダプター
  storage: new RedisStorageAdapter({
    url: process.env.REDIS_URL,
  }),
  
  // カスタムロギング
  logger: {
    info: (msg) => console.log(msg),
    error: (msg) => console.error(msg),
  },
  
  // 認証ハンドラー
  authenticate: async (request) => {
    const token = request.headers.authorization
    return validateToken(token)
  },
})

// クライアント接続の処理
server.on('connection', (client) => {
  console.log(`Client connected: ${client.id}`)
  
  client.on('sync', async (docId) => {
    const doc = await loadDocument(docId)
    client.send(doc)
  })
})

パフォーマンス最適化

// 変更のバッチ処理
const batchChanges = (changes: Automerge.Change[]) => {
  return Automerge.Change.squash(changes)
}

// メモリ使用量の最適化
const optimizeMemory = (doc: Automerge.Doc<any>) => {
  return Automerge.compact(doc)
}

// 大規模ドキュメントの処理
const handleLargeDoc = async (docUrl: AutomergeUrl) => {
  const doc = await repo.find(docUrl)
  if (doc) {
    // 変更履歴の圧縮
    const compacted = Automerge.compact(doc)
    
    // チャンク分割による効率的な同期
    const chunks = Automerge.save(compacted)
    for (const chunk of chunks) {
      await repo.saveChunk(docUrl, chunk)
    }
  }
}

監視とデバッグ

メトリクス収集

// Automergeメトリクスコレクター
class MetricsCollector {
  collect(doc: Automerge.Doc<any>) {
    return {
      changeCount: doc.getHistory().length,
      byteSize: Automerge.save(doc).length,
      actors: doc.getActors().length,
      conflicts: doc.getConflicts().length
    }
  }
}

// Prometheusエクスポーター
const exportMetrics = (metrics: DocMetrics) => {
  prometheus.gauge({
    name: 'automerge_doc_size_bytes',
    help: 'Document size in bytes',
    value: metrics.byteSize
  })
  
  prometheus.counter({
    name: 'automerge_changes_total',
    help: 'Total number of changes',
    value: metrics.changeCount
  })
}

デバッグツール

// デバッグモードの有効化
const enableDebugMode = (repo: Automerge.Repo) => {
  repo.registerDebugHandler((event) => {
    console.log('[Automerge Debug]', {
      type: event.type,
      docId: event.docId,
      changeset: event.changes?.length,
      timestamp: Date.now()
    })
  })
}

// 変更履歴の検査
const inspectHistory = (doc: Automerge.Doc<any>) => {
  const history = doc.getHistory()
  return history.map(change => ({
    actor: change.actor,
    timestamp: change.timestamp,
    deps: change.deps,
    operations: change.operations
  }))
}

セキュリティ考慮事項

アクセス制御

// カスタム認証インテグレーション
const setupSecureRepo = (config: SecurityConfig) => {
  return new Automerge.Repo({
    network: [
      new SecureNetworkAdapter({
        validateToken: config.validateToken,
        encryptChanges: config.encryptChanges
      })
    ],
    storage: new EncryptedStorageAdapter({
      key: config.storageKey
    })
  })
}

// 変更の検証
const validateChange = (change: Automerge.Change) => {
  // アクターの検証
  if (!isValidActor(change.actor)) {
    throw new Error('Invalid actor')
  }
  
  // 操作の検証
  for (const op of change.operations) {
    if (!isAllowedOperation(op)) {
      throw new Error('Operation not allowed')
    }
  }
}

参考リンク