/**
* 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()
}