OverviewFilesCommitsIssuesDocsReleasesPeers

pearbrowser-desktop / backend / browser-state-apply.cjs

52 lines · javascript ref:
// Deterministic reducer for synced browser state (bookmarks) — PURE.
//
// Folds an ORDERED op log into the materialized bookmark set. No wall-clock:
// order comes from Autobase linearization, modeled in tests by a node tag
// (writer + seq). Conflict rules:
//   · add vs remove → later op in order wins (keyed by url)
//   · bookmark record → whole-record replacement (no field merge)
//   · url is stable identity
//   · unknown op types/versions → retained in the log, ignored in the view

const { OP_ADD, OP_REMOVE, validateOp, sanitizeBookmark } = require('./browser-state-ops.cjs')

function cmp (a, b) { return a < b ? -1 : a > b ? 1 : 0 }
function stableStringify (op) {
  try { return JSON.stringify(op, Object.keys(op).sort()) } catch { return String(op) }
}

// Total order over Autobase-style tagged nodes — { writer, seq, op } — with
// NO wall-clock input. Both replicas see the same tags, so both agree.
function linearize (tagged) {
  return [...tagged].sort((a, b) =>
    (a.seq - b.seq) ||
    cmp(String(a.writer), String(b.writer)) ||
    cmp(stableStringify(a.op), stableStringify(b.op))
  )
}

function applyView (orderedOps) {
  const bookmarks = new Map() // url → record (insertion-ordered)
  for (const op of orderedOps || []) {
    const verdict = validateOp(op)
    if (!verdict.ok) continue
    if (op.type === OP_ADD) {
      bookmarks.set(op.url, { ...sanitizeBookmark(op.bookmark), url: op.url })
    } else if (op.type === OP_REMOVE) {
      bookmarks.delete(op.url)
    }
  }
  return { bookmarks: [...bookmarks.values()] }
}

function applyTagged (tagged) {
  return applyView(linearize(tagged).map((t) => t.op))
}

function toStateData (view) {
  const bookmarks = Array.isArray(view && view.bookmarks) ? view.bookmarks : []
  return { bookmarks, count: { bookmarks: bookmarks.length } }
}

module.exports = { linearize, applyView, applyTagged, toStateData }