OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / scripts / old-apply / repo-apply.js

175 lines · javascript ref:
/**
 * Deterministic Apply Function for Linked Repo Autobase
 *
 * Processes linearized operations and updates the Hyperbee view.
 * This function MUST be fully deterministic — it may be called
 * multiple times as Autobase reorders causal forks.
 *
 * View schema (Hyperbee keys):
 *   writer:{userId}              → { role, serverKey, identityKey, proof, addedBy, addedAt }
 *   ref:{refPath}                → { hash, updatedBy, timestamp }
 *   issue:{id}                   → { id, title, body, author, status, labels, images, createdAt, updatedAt }
 *   comment:{issueId}:{id}       → { body, author, createdAt }
 *   label:{name}                 → { color, description, displayName }
 *   meta:{key}                   → { value }
 *   counter:issues               → { next: number }
 */

import { decodeOp } from './repo-operations.js'

/**
 * Apply function for Autobase
 *
 * @param {Array} nodes - Linearized operation nodes
 * @param {Hyperbee} view - Hyperbee view to update
 * @param {Object} host - Autobase host (for addWriter/removeWriter)
 */
export async function apply (nodes, view, host) {
  const batch = view.batch()

  for (const node of nodes) {
    const op = decodeOp(node.value)

    switch (op.type) {
      case 'add-writer': {
        await batch.put(`writer:${op.userId}`, {
          role: op.role,
          serverKey: op.serverKey,
          identityKey: op.identityKey || null,
          proof: op.proof || null,
          addedBy: op.addedBy || null,
          addedAt: op.timestamp || Date.now()
        })
        // Register as Autobase writer. Validation happens at op creation
        // (LinkedRepo.addWriter checks key length). The drain loop patch in
        // Autobase prevents infinite hangs if a bad key gets through.
        if (op.serverKey && op.role !== 'mirror') {
          try {
            await host.addWriter(Buffer.from(op.serverKey, 'hex'), {
              indexer: op.role === 'owner'
            })
          } catch (err) {
            console.error(`[LinkedRepo:apply] add-writer failed for ${op.serverKey.slice(0, 16)}...: ${err.message}`)
          }
        }
        break
      }

      case 'remove-writer': {
        const existing = await batch.get(`writer:${op.userId}`)
        if (existing) {
          const writerData = existing.value
          await batch.del(`writer:${op.userId}`)
          if (writerData.serverKey) {
            try {
              await host.removeWriter(Buffer.from(writerData.serverKey, 'hex'))
            } catch (err) {
              console.error(`[LinkedRepo:apply] remove-writer failed for ${writerData.serverKey.slice(0, 16)}...: ${err.message}`)
            }
          }
        }
        break
      }

      case 'update-ref': {
        await batch.put(`ref:${op.ref}`, {
          hash: op.hash,
          updatedBy: op.userId,
          timestamp: op.timestamp || Date.now()
        })
        break
      }

      case 'delete-ref': {
        await batch.del(`ref:${op.ref}`)
        break
      }

      case 'create-issue': {
        await batch.put(`issue:${op.id}`, {
          id: op.id,
          title: op.title,
          body: op.body || '',
          author: op.userId,
          status: op.status || 'open',
          labels: op.labels || [],
          images: op.images || [],
          createdAt: op.timestamp || Date.now(),
          updatedAt: op.timestamp || Date.now()
        })
        // Update counter
        const counter = await batch.get('counter:issues')
        const currentNext = counter ? counter.value.next : 0
        if (op.id >= currentNext) {
          await batch.put('counter:issues', { next: op.id + 1 })
        }
        break
      }

      case 'update-issue': {
        const entry = await batch.get(`issue:${op.id}`)
        if (entry) {
          const issue = entry.value
          if (op.title !== undefined) issue.title = op.title
          if (op.body !== undefined) issue.body = op.body
          if (op.status !== undefined) issue.status = op.status
          if (op.labels !== undefined) issue.labels = op.labels
          if (op.images !== undefined) issue.images = op.images
          issue.updatedAt = op.timestamp || Date.now()
          await batch.put(`issue:${op.id}`, issue)
        }
        break
      }

      case 'create-comment': {
        await batch.put(`comment:${op.issueId}:${op.commentId}`, {
          body: op.body,
          author: op.userId,
          createdAt: op.timestamp || Date.now()
        })
        // Touch parent issue updatedAt
        const issueEntry = await batch.get(`issue:${op.issueId}`)
        if (issueEntry) {
          const issue = issueEntry.value
          issue.updatedAt = op.timestamp || Date.now()
          await batch.put(`issue:${op.issueId}`, issue)
        }
        break
      }

      case 'create-label': {
        await batch.put(`label:${op.name}`, {
          color: op.color || '#ccc',
          description: op.description || '',
          displayName: op.displayName || op.name
        })
        break
      }

      case 'update-label': {
        const labelEntry = await batch.get(`label:${op.name}`)
        if (labelEntry) {
          const label = labelEntry.value
          if (op.color !== undefined) label.color = op.color
          if (op.description !== undefined) label.description = op.description
          if (op.displayName !== undefined) label.displayName = op.displayName
          await batch.put(`label:${op.name}`, label)
        }
        break
      }

      case 'delete-label': {
        await batch.del(`label:${op.name}`)
        break
      }

      case 'set-meta': {
        await batch.put(`meta:${op.key}`, { value: op.value })
        break
      }
    }
  }

  await batch.flush()
}