fix concurrent load and update, add support for gc'ing elements

This commit is contained in:
Andre Staltz 2024-01-19 14:56:23 +02:00
parent 633682db8a
commit 7a98e3cbcc
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
2 changed files with 59 additions and 20 deletions

70
main.js
View File

@ -106,14 +106,6 @@ async function loadAccount() {
globalAccountID = id globalAccountID = id
await p(peer.set.load)(id) await p(peer.set.load)(id)
await p(peer.dict.load)(id) await p(peer.dict.load)(id)
peer.conductor.start(
id,
[
['profile@dict', 'zooboardElements@newest-100', 'hubs@set'],
['profile@dict', 'zooboardElements@newest-100'],
],
64_000_000
)
// Read hubs // Read hubs
const multiaddrs = peer.set.values('hubs') const multiaddrs = peer.set.values('hubs')
@ -134,38 +126,82 @@ async function setProfileName(ev, name) {
return name return name
} }
async function writeElements(ev, actions) { async function writeElements(ev, elements) {
await loadAccount() // FIXME: ideally the frontend shouldn't do this
if (globalAccountID === null) throw new Error('account not loaded') if (globalAccountID === null) throw new Error('account not loaded')
for (const action of actions) { for (const element of elements) {
await p(peer.db.feed.publish)({ await p(peer.db.feed.publish)({
account: globalAccountID, account: globalAccountID,
domain: 'zooboardElements', domain: 'zooboardElements',
data: action, data: element,
}) })
} }
} }
let hasSubscribedToReadElements = false
function subscribeToReadElements() { function subscribeToReadElements() {
if (hasSubscribedToReadElements) return
hasSubscribedToReadElements = true
// Load initial elements and inform renderer
const elementsByID = new Map() const elementsByID = new Map()
for (const msg of peer.db.msgs()) { const msgIDToElemID = new Map()
for (const { id: msgID, msg } of peer.db.records()) {
if (msg.data && msg.metadata.domain === 'zooboardElements') { if (msg.data && msg.metadata.domain === 'zooboardElements') {
const { id, isDeleted } = msg.data const { id: elemID, isDeleted } = msg.data
if (isDeleted) { if (isDeleted) {
elementsByID.delete(id) elementsByID.delete(elemID)
} else { } else {
elementsByID.set(id, msg.data) msgIDToElemID.set(msgID, elemID)
elementsByID.set(elemID, msg.data)
} }
} }
} }
const initialElements = [...elementsByID.values()] const initialElements = [...elementsByID.values()]
mainWindow.webContents.send('readElements', initialElements) mainWindow.webContents.send('readElements', initialElements)
peer.db.onRecordAdded(({ msg }) => { // Subscribe to new elements and inform renderer
peer.db.onRecordAdded(({ id: msgID, msg }) => {
if (msg.data && msg.metadata.domain === 'zooboardElements') { if (msg.data && msg.metadata.domain === 'zooboardElements') {
const { id: elemID, isDeleted } = msg.data
if (isDeleted) {
elementsByID.delete(elemID)
} else {
msgIDToElemID.set(msgID, elemID)
elementsByID.set(elemID, msg.data)
}
mainWindow.webContents.send('readElements', [msg.data]) mainWindow.webContents.send('readElements', [msg.data])
} }
}) })
// Subscribe to deleted elements and inform renderer
peer.db.onRecordDeletedOrErased((msgID) => {
const elemID = msgIDToElemID.get(msgID)
if (!elemID) return
msgIDToElemID.delete(msgID)
// Is there some other msgID that supports this elemID? If so, bail out
for (const [, remainingElemID] of msgIDToElemID) {
if (remainingElemID === elemID) {
return
}
}
// If not, delete the element
elementsByID.delete(elemID)
mainWindow.webContents.send('readElements', [
{ id: elemID, isDeleted: true },
])
})
// Finally safe to kickstart replication and garbage collection
setTimeout(() => {
peer.conductor.start(
globalAccountID,
[
['profile@dict', 'zooboardElements@newest-100', 'hubs@set'],
['profile@dict', 'zooboardElements@newest-100'],
],
64_000
)
}, 32)
} }
async function scheduleWithHub(multiaddr) { async function scheduleWithHub(multiaddr) {

View File

@ -3,6 +3,9 @@ import { Excalidraw } from '@excalidraw/excalidraw'
import './App.css' import './App.css'
import debounce from 'debounce' import debounce from 'debounce'
const elemsPersisted = new Map()
let sceneInitialized = false
function MyAccount() { function MyAccount() {
const nameInput = createRef() const nameInput = createRef()
const [loaded, setLoaded] = useState(false) const [loaded, setLoaded] = useState(false)
@ -52,7 +55,6 @@ function MyAccount() {
} }
function App() { function App() {
const elemsPersisted = new Map()
const [excalidrawAPI, setExcalidrawAPI] = useState(null) const [excalidrawAPI, setExcalidrawAPI] = useState(null)
function loadExcalidraw(api) { function loadExcalidraw(api) {
@ -67,14 +69,15 @@ function App() {
} }
} }
api.updateScene({ elements: [...elemsPersisted.values()] }) api.updateScene({ elements: [...elemsPersisted.values()] })
if (!sceneInitialized) sceneInitialized = true
}) })
} }
const updateElements = debounce((elems) => { const updateElements = debounce((elems) => {
if (excalidrawAPI) return if (!sceneInitialized) return
const actions = [] const actions = []
for (const elem of elems) { for (const elem of elems) {
const oldVersion = elemsPersisted.get(elem.id)?.version ?? 0 const oldVersion = elemsPersisted.get(elem.id)?.version ?? -1
if (elem.version > oldVersion) { if (elem.version > oldVersion) {
actions.push(elem) actions.push(elem)
elemsPersisted.set(elem.id, { ...elem }) elemsPersisted.set(elem.id, { ...elem })