-
Moritz Langenstein authored
Conflicts: package-lock.json src/room.js
Moritz Langenstein authoredConflicts: package-lock.json src/room.js
room.js 6.37 KiB
import uuidv4 from "uuid/v4"
import yArray from "y-array"
import yMap from "y-map"
import yUnion, { Union } from "./y-union.js"
import yMemory from "y-memory"
import Y from "yjs"
import yP2PMesh from "./y-p2p-mesh.js"
yMemory(Y)
Y.Struct.Union = Union
yUnion(Y)
yMap(Y)
yArray(Y)
yP2PMesh(Y)
import { spreadErasureIntervals, flattenErasureIntervals } from "./erasure.js"
/* webpack should NOT import the yaeti NodeJS polyfill */
// #!if false
import { EventTarget } from "yaeti"
// #!endif
class Room extends EventTarget {
constructor(name) {
super()
this.name = name
this._y = null
this.ownID = null
this.undoStack = []
}
disconnect() {
this._y.destroy()
}
addPath([x, y, w, colour]) {
const id = uuidv4()
this.shared.strokePoints.set(id, Y.Array).push([[x, y, w, colour]])
this.shared.eraseIntervals.set(id, Y.Union)
this.undoStack.push([id, 0, 0])
this.dispatchEvent(new CustomEvent("undoEnabled"))
return id
}
extendPath(id, [x, y, w, colour]) {
const path = this.shared.strokePoints.get(id)
path.push([[x, y, w, colour]])
if (path.length == 2) {
this.undoStack[this.undoStack.length - 1] = [id, 0, 1]
} else {
this.undoStack.push([id, path.length - 2, path.length - 1])
}
this.dispatchEvent(new CustomEvent("undoEnabled"))
}
extendErasureIntervals(pathID, pointID, newIntervals) {
this.shared.eraseIntervals
.get(pathID)
.merge(flattenErasureIntervals({ [pointID]: newIntervals }))
}
replacePath(pathID, newPoints) {
this.fastUndo(true)
newPoints.forEach((point) => this.extendPath(pathID, point))
this.undoStack.splice(this.undoStack.length - newPoints.length, 1)
}
undo() {
const operation = this.undoStack.pop()
if (!operation) return
const [id, ...interval] = operation
this.shared.eraseIntervals.get(id).merge([interval])
}
fastUndo(forReplacing = false) {
let from = this.undoStack.length - 1
if (from < 0) return
// eslint-disable-next-line no-unused-vars
const [id, _, end] = this.undoStack[from]
const endErasing = forReplacing ? end + 1 : end
for (; from >= 0; from--) {
if (this.undoStack[from][0] != id) {
from++
break
}
}
this.undoStack = this.undoStack.slice(0, Math.max(0, from))
this.shared.eraseIntervals.get(id).merge([[0, endErasing]])
}
canUndo() {
return this.undoStack.length > 0
}
getPaths() {
const paths = new Map()
for (const id of this.shared.strokePoints.keys()) {
paths.set(id, this._generatePath(id))
}
return paths
}
getErasureIntervals(pathID) {
return this._generateRemovedIntervals(pathID)
}
getPathPoints(pathID) {
return this._generatePath(pathID)
}
get shared() {
return this._y.share
}
_generatePath(id) {
const points = this.shared.strokePoints.get(id)
if (!points) return []
return points.toArray()
}
_generateRemovedIntervals(id) {
const intervals = this.shared.eraseIntervals.get(id)
if (!intervals) return []
return spreadErasureIntervals(intervals.get())
}
inviteUser(id) {
this._y.connector.connectToPeer(id)
}
async _initialise(connection) {
this._y = await Y({
db: {
name: "memory",
},
connector: {
name: "p2p-mesh",
connection,
url: "/",
room: this.name,
mesh: {
minPeers: 4,
maxPeers: 8,
},
handshake: {
initial: 100,
interval: 500,
},
heartbeat: {
interval: 500,
minimum: 1000,
timeout: 10000,
},
onUserEvent: (event) => {
if (event.action == "userConnection") {
const { id, quality } = event
this.dispatchEvent(
new CustomEvent("userConnection", { detail: { id, quality } }),
)
} else if (event.action == "userID") {
const { user: id } = event
this.ownID = id
this.dispatchEvent(new CustomEvent("allocateOwnID", { detail: id }))
} else if (event.action == "userJoined") {
const { user: id } = event
this.dispatchEvent(new CustomEvent("userJoin", { detail: id }))
} else if (event.action == "userLeft") {
const { user: id } = event
this.dispatchEvent(new CustomEvent("userLeave", { detail: id }))
} else if (event.action === "peerSyncedWithUs") {
const { user: id } = event
this.dispatchEvent(
new CustomEvent("peerSyncedWithUs", { detail: id }),
)
} else if (event.action === "waitingForSyncStep") {
const { user: id } = event
this.dispatchEvent(
new CustomEvent("waitingForSyncStep", { detail: id }),
)
} else if (event.action === "weSyncedWithPeer") {
const { user: id } = event
this.dispatchEvent(
new CustomEvent("weSyncedWithPeer", { detail: id }),
)
}
},
},
share: {
strokePoints: "Map",
eraseIntervals: "Map",
},
})
const dispatchRemovedIntervalsEvent = (lineEvent) => {
const id = lineEvent.name
const intervals = this._generateRemovedIntervals(id)
const detail = { id, intervals }
this.dispatchEvent(
new CustomEvent("removedIntervalsChange", {
detail,
}),
)
}
const dispatchPathUpdateEvent = (lineEvent) => {
const id = lineEvent.name
const points = this._generatePath(id)
const detail = { id, points }
this.dispatchEvent(new CustomEvent("addOrUpdatePath", { detail }))
}
this.shared.strokePoints.observe((lineEvent) => {
if (lineEvent.type == "add") {
dispatchPathUpdateEvent(lineEvent)
lineEvent.value.observe((pointEvent) => {
if (pointEvent.type == "insert") {
dispatchPathUpdateEvent(lineEvent)
}
})
}
})
this.shared.eraseIntervals.observe((lineEvent) => {
if (lineEvent.type == "add") {
dispatchRemovedIntervalsEvent(lineEvent)
lineEvent.value.observe(() => {
dispatchRemovedIntervalsEvent(lineEvent)
})
}
})
}
}
export const connect = async (roomName, connection) => {
const room = new Room(roomName)
await room._initialise(connection)
return room
}