A text crdt for Sovereignbase
Try writing in different elements in multiple browser tabs.
Sync mode
import {
CRText,
BeforeInputStreamAdapter,
ChangeStreamAdapter,
} from '@sovereignbase/convergent-replicated-text'
import { StationClient } from '@sovereignbase/station-client'
const station = new StationClient()
const snapshot = JSON.parse(localStorage.getItem('state')) ?? undefined
const frontiers = JSON.parse(localStorage.getItem('frontiers')) ?? undefined
const text = new CRText(snapshot)
if (frontiers) {
void text.garbageCollect(frontiers)
}
text.addEventListener('snapshot', (ev) => {
void localStorage.setItem('state', JSON.stringify(ev.detail))
})
text.addEventListener('ack', (ev) => {
void localStorage.setItem('frontiers', JSON.stringify([ev.detail]))
})
const elements = [
document.getElementById('textarea-element'),
document.getElementById('input-element'),
document.getElementById('html-element'),
]
const mergeButton = document.getElementById('merge-button')
const syncModeButtons = document.querySelectorAll('[data-sync-mode]')
let syncMode = 'auto'
let manualInputElement
let isFlushingManualSync = false
const pendingLocalChanges = []
const pendingOutgoingDeltas = []
const pendingIncomingDeltas = []
function flushManualUi() {
for (const { detail, sourceElement } of pendingLocalChanges.splice(0)) {
for (const element of elements) {
if (element === sourceElement) continue
void ChangeStreamAdapter({ detail }, element)
}
}
}
function flushManualSync() {
flushManualUi()
isFlushingManualSync = true
try {
for (const delta of pendingIncomingDeltas.splice(0)) {
void text.merge(delta)
}
} finally {
isFlushingManualSync = false
}
for (const delta of pendingOutgoingDeltas.splice(0)) {
void station.relay(delta)
}
}
function setSyncMode(nextMode) {
syncMode = nextMode
mergeButton.hidden = syncMode !== 'manual'
for (const button of syncModeButtons) {
button.setAttribute(
'aria-pressed',
button.dataset.syncMode === syncMode ? 'true' : 'false'
)
}
if (syncMode === 'auto') {
flushManualSync()
}
}
text.addEventListener('change', (event) => {
if (syncMode === 'manual' && !isFlushingManualSync) {
if (manualInputElement) {
void ChangeStreamAdapter(event, manualInputElement)
}
pendingLocalChanges.push({
detail: event.detail,
sourceElement: manualInputElement,
})
} else {
for (const element of elements) {
void ChangeStreamAdapter(event, element)
}
}
void text.snapshot()
void text.acknowledge()
})
for (const element of elements) {
element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement
? (element.value = text)
: (element.textContent = text)
void element.addEventListener(
'beforeinput',
(event) => {
manualInputElement = syncMode === 'manual' ? element : undefined
try {
void BeforeInputStreamAdapter(event, text)
} finally {
manualInputElement = undefined
}
}
)
}
mergeButton.addEventListener('click', flushManualSync)
for (const button of syncModeButtons) {
button.addEventListener('click', () => {
const nextMode = button.dataset.syncMode
if (nextMode === 'auto' || nextMode === 'manual') {
setSyncMode(nextMode)
}
})
}
setSyncMode('auto')
text.addEventListener('delta', (ev) => {
if (syncMode === 'auto') {
void station.relay(ev.detail)
return
}
pendingOutgoingDeltas.push(ev.detail)
})
station.addEventListener('message', (ev) => {
if (syncMode === 'auto') {
void text.merge(ev.detail)
return
}
pendingIncomingDeltas.push(ev.detail)
})