157 lines
4.2 KiB
JavaScript
157 lines
4.2 KiB
JavaScript
require('bare-process/global')
|
|
const EventEmitter = require('bare-events')
|
|
const { assertNonEmpty } = require('../../_shared/lib/errors.js')
|
|
const { initModuleSwarm, gossipSend } = require('../../_shared/p2p-bare.js')
|
|
const { crdtStats } = require('../../_shared/crdt-base.js')
|
|
|
|
const PROTOCOL = 'crdt-rga-text/v1'
|
|
|
|
class HyperP2PCrdtRgaText extends EventEmitter {
|
|
constructor (opts = {}) {
|
|
super()
|
|
this.topic = opts.topic || null
|
|
this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair()
|
|
this._nodes = new Map()
|
|
this._order = []
|
|
this._stats = { ops: 0, gossipIn: 0, gossipOut: 0 }
|
|
this.swarm = null
|
|
this._peerMsgs = null
|
|
}
|
|
|
|
insert (index, charId, char) {
|
|
return this._applyInsert(index, charId, char, true)
|
|
}
|
|
|
|
delete (charId) {
|
|
return this._applyDelete(charId, true)
|
|
}
|
|
|
|
_applyInsert (index, charId, char, gossip) {
|
|
if (index < 0) throw new Error('index must be non-negative')
|
|
assertNonEmpty(charId, 'charId')
|
|
if (char == null || char === '') throw new Error('char required')
|
|
const id = String(charId)
|
|
if (!this._nodes.has(id)) {
|
|
this._nodes.set(id, { char: String(char), deleted: false })
|
|
if (!this._order.includes(id)) this._order.push(id)
|
|
} else if (this._nodes.get(id).deleted) {
|
|
this._nodes.get(id).deleted = false
|
|
this._nodes.get(id).char = String(char)
|
|
}
|
|
const visible = this._visibleIds()
|
|
const curIdx = visible.indexOf(id)
|
|
if (curIdx >= 0) visible.splice(curIdx, 1)
|
|
const at = Math.min(index, visible.length)
|
|
visible.splice(at, 0, id)
|
|
this._order = [...visible, ...this._order.filter((x) => !visible.includes(x))]
|
|
this._stats.ops++
|
|
if (gossip) {
|
|
gossipSend(this, { type: 'crdt-rga-text-sync', op: 'insert', charId: id, char: String(char), index: at })
|
|
this._stats.gossipOut++
|
|
}
|
|
return id
|
|
}
|
|
|
|
_applyDelete (charId, gossip) {
|
|
assertNonEmpty(charId, 'charId')
|
|
const id = String(charId)
|
|
const node = this._nodes.get(id)
|
|
if (!node || node.deleted) return false
|
|
node.deleted = true
|
|
this._stats.ops++
|
|
if (gossip) {
|
|
gossipSend(this, { type: 'crdt-rga-text-sync', op: 'delete', charId: id })
|
|
this._stats.gossipOut++
|
|
}
|
|
return true
|
|
}
|
|
|
|
toString () {
|
|
return this._visibleIds().map((id) => this._nodes.get(id).char).join('')
|
|
}
|
|
|
|
length () {
|
|
return this.toString().length
|
|
}
|
|
|
|
charAt (index) {
|
|
const ids = this._visibleIds()
|
|
const id = ids[index]
|
|
return id ? this._nodes.get(id).char : ''
|
|
}
|
|
|
|
visibleIds () {
|
|
return this._visibleIds()
|
|
}
|
|
|
|
merge (remote) {
|
|
const ops = Array.isArray(remote) ? remote : (remote?.ops || [])
|
|
let n = 0
|
|
for (const op of ops) {
|
|
if (!op || !op.op) continue
|
|
if (op.op === 'insert') {
|
|
this._applyInsert(op.index || 0, op.charId, op.char, false)
|
|
n++
|
|
}
|
|
if (op.op === 'delete' && this._applyDelete(op.charId, false)) n++
|
|
}
|
|
return n
|
|
}
|
|
|
|
snapshot () {
|
|
return {
|
|
nodes: Object.fromEntries([...this._nodes].map(([id, n]) => [id, { ...n }])),
|
|
order: [...this._order]
|
|
}
|
|
}
|
|
|
|
insertBatch (ops) {
|
|
if (!Array.isArray(ops)) throw new Error('ops array required')
|
|
return ops.map((op) => this.insert(op.index, op.charId, op.char))
|
|
}
|
|
|
|
clearAll () {
|
|
const n = this._nodes.size
|
|
this._nodes.clear()
|
|
this._order = []
|
|
return n
|
|
}
|
|
|
|
_visibleIds () {
|
|
return this._order.filter((id) => {
|
|
const n = this._nodes.get(id)
|
|
return n && !n.deleted
|
|
})
|
|
}
|
|
|
|
_onGossip (d) {
|
|
if (!d || d.type !== 'crdt-rga-text-sync' || !d.op) return
|
|
this._stats.gossipIn++
|
|
if (d.op === 'insert') this._applyInsert(d.index || 0, d.charId, d.char, false)
|
|
if (d.op === 'delete') this._applyDelete(d.charId, false)
|
|
}
|
|
|
|
getStats () {
|
|
return crdtStats(this._stats, PROTOCOL, { length: this.length() })
|
|
}
|
|
|
|
async ready () {
|
|
if (this.swarm || !this.topic) return this
|
|
await initModuleSwarm(this, {
|
|
keyPair: this.keyPair,
|
|
topic: this.topic,
|
|
protocol: PROTOCOL,
|
|
onmessage: (d) => this._onGossip(d)
|
|
})
|
|
return this
|
|
}
|
|
|
|
async close () {
|
|
if (this.swarm) await this.swarm.destroy().catch(() => {})
|
|
this.swarm = null
|
|
this._peerMsgs = null
|
|
}
|
|
}
|
|
|
|
module.exports = { HyperP2PCrdtRgaText, PROTOCOL }
|