diff --git a/__tests__/benchmark.data.js b/__tests__/benchmark.data.js index 97283a38890b0bbe53c1a7d8dc9aa781db52927d..86df444825ed4c4c413f9c8ac57aa43fd4d526c8 100644 --- a/__tests__/benchmark.data.js +++ b/__tests__/benchmark.data.js @@ -1,128 +1,130 @@ +export function createMessageReceivedEvent( + message, + channel = "y-js", + uid = "moritz", +) { + return { + detail: { + uid, + channel, + message, + }, + } +} + export const handshake = { - uid: "tiger", - channel: "tw-ml", - message: { - uuid: "42c4566d-0cfe-4c00-a645-254b7887e477", - message: Uint8Array.of(162, 109, 108), - slice: 0, - length: 1, - compressed: false, - }, + uuid: "42c4566d-0cfe-4c00-a645-254b7887e477", + message: Uint8Array.of(162, 109, 108), + slice: 0, + length: 1, + compressed: false, } export const syncStep1 = { - uid: "tiger", - channel: "y-js", - message: { - uuid: "6e20b20d-e1d8-405d-8a61-d56cb1c47a24", - message: Uint8Array.of( - 120, - 156, - 107, - 93, - 82, - 82, - 89, - 144, - 186, - 186, - 184, - 50, - 47, - 89, - 161, - 184, - 36, - 181, - 64, - 193, - 112, - 69, - 113, - 73, - 98, - 73, - 106, - 112, - 106, - 73, - 195, - 202, - 148, - 212, - 156, - 84, - 8, - 115, - 125, - 65, - 81, - 126, - 73, - 126, - 114, - 126, - 78, - 88, - 106, - 81, - 113, - 102, - 126, - 30, - 247, - 146, - 196, - 210, - 146, - 140, - 3, - 0, - 80, - 113, - 26, - 230, - ), - slice: 0, - length: 1, - compressed: true, - }, + uuid: "6e20b20d-e1d8-405d-8a61-d56cb1c47a24", + message: Uint8Array.of( + 120, + 156, + 107, + 93, + 82, + 82, + 89, + 144, + 186, + 186, + 184, + 50, + 47, + 89, + 161, + 184, + 36, + 181, + 64, + 193, + 112, + 69, + 113, + 73, + 98, + 73, + 106, + 112, + 106, + 73, + 195, + 202, + 148, + 212, + 156, + 84, + 8, + 115, + 125, + 65, + 81, + 126, + 73, + 126, + 114, + 126, + 78, + 88, + 106, + 81, + 113, + 102, + 126, + 30, + 247, + 146, + 196, + 210, + 146, + 140, + 3, + 0, + 80, + 113, + 26, + 230, + ), + slice: 0, + length: 1, + compressed: true, } export const syncDone = { - uid: "tiger", - channel: "y-js", - message: { - message: Uint8Array.of( - 120, - 156, - 107, - 92, - 82, - 82, - 89, - 144, - 186, - 178, - 184, - 50, - 47, - 89, - 33, - 37, - 63, - 47, - 21, - 0, - 64, - 79, - 7, - 20, - ), - slice: 0, - length: 1, - compressed: true, - }, + message: Uint8Array.of( + 120, + 156, + 107, + 92, + 82, + 82, + 89, + 144, + 186, + 178, + 184, + 50, + 47, + 89, + 33, + 37, + 63, + 47, + 21, + 0, + 64, + 79, + 7, + 20, + ), + slice: 0, + length: 1, + compressed: true, } export const dotDraw = [[209, 88, 5.000000000000001, "#0000ff"]] diff --git a/__tests__/benchmark.test.js b/__tests__/benchmark.test.js index dc9ec46e8bcbd4e8dd97cf96a582568f90c9e090..7784d3a5627c7649ac8887ed72761099d2577e23 100644 --- a/__tests__/benchmark.test.js +++ b/__tests__/benchmark.test.js @@ -13,10 +13,11 @@ import MockConnection, { terminatePeer, destructor, addEventListener, - eventListeners, + getEventListener, } from "../src/connection/MockConnection.js" import { + createMessageReceivedEvent, handshake, syncStep1, syncDone, @@ -47,6 +48,8 @@ function printBenchmark(title, iterations, results) { } let room = null +let updateRoom = null +let syncRoom = null describe("drawing app mesh", () => { beforeEach(async () => { @@ -60,13 +63,34 @@ describe("drawing app mesh", () => { addEventListener.mockClear() MockConnection.mockClear() - room = await connect("data", MockConnection) - eventListeners.get("messageReceived")({ detail: handshake }) + room = await connect("room", MockConnection) + getEventListener( + "room", + "messageReceived", + )(createMessageReceivedEvent(handshake, "tw-ml")) + + updateRoom = await connect("update", MockConnection) + getEventListener( + "update", + "messageReceived", + )(createMessageReceivedEvent(handshake, "tw-ml")) + + syncRoom = await connect("sync", MockConnection) + getEventListener( + "sync", + "messageReceived", + )(createMessageReceivedEvent(handshake, "tw-ml")) }) afterEach(() => { room.disconnect() room = null + + updateRoom.disconnect() + updateRoom = null + + syncRoom.disconnect() + syncRoom = null }) it("benchmarks a single draw and erase update sequentially", () => { @@ -160,7 +184,10 @@ describe("drawing app mesh", () => { prevTime = process.hrtime() - eventListeners.get("messageReceived")({ detail: syncStep1 }) + getEventListener( + "room", + "messageReceived", + )(createMessageReceivedEvent(syncStep1)) }), ) }) @@ -269,24 +296,41 @@ describe("drawing app mesh", () => { prevTime = process.hrtime() - eventListeners.get("messageReceived")({ detail: syncStep1 }) + getEventListener( + "room", + "messageReceived", + )(createMessageReceivedEvent(syncStep1)) }), ) }) it("communicates a single draw and erase update", () => { + jest.setTimeout(30000) + let dotID - let syncPackets = 0 + const addPackets = [] + const erasePackets = [] + const syncPackets = [] let syncDonePacket = -1 + const updatePaths = {} + const updateIntervals = {} + const syncPaths = {} + const syncIntervals = {} + let timeout return new Promise((resolve) => { + // Draw the dot path broadcastListener.callback = (channel, message) => { expect(channel).toEqual("y-js") expect(message.message instanceof Uint8Array).toBe(true) - resolve() + + addPackets.push(message) + + clearTimeout(timeout) + timeout = setTimeout(() => resolve(), 1000) } dotID = room.addPath(dotDraw[0]) @@ -294,10 +338,15 @@ describe("drawing app mesh", () => { .then( () => new Promise((resolve) => { + // Erase the dot path broadcastListener.callback = (channel, message) => { expect(channel).toEqual("y-js") expect(message.message instanceof Uint8Array).toBe(true) - resolve() + + erasePackets.push(message) + + clearTimeout(timeout) + timeout = setTimeout(() => resolve(), 1000) } room.extendErasureIntervals(dotID, dotErase[0][0], dotErase[0][1]) @@ -306,32 +355,160 @@ describe("drawing app mesh", () => { .then( () => new Promise((resolve) => { + // Request a sync step 2 sendListener.callback = (uid, channel, message) => { - expect(uid).toEqual("tiger") + expect(uid).toEqual("moritz") expect(channel).toEqual("y-js") expect(message.message instanceof Uint8Array).toBe(true) - syncPackets += 1 + syncPackets.push(message) if ( - message.message.length == syncDone.message.message.length && - JSON.stringify(Object.assign(message, { uuid: undefined })) == - JSON.stringify(syncDone.message) + message.message.length == syncDone.message.length && + JSON.stringify( + Object.assign({}, message, { uuid: undefined }), + ) == JSON.stringify(syncDone) ) { expect(syncDonePacket).toEqual(-1) - syncDonePacket = syncPackets + syncDonePacket = syncPackets.length } clearTimeout(timeout) timeout = setTimeout(() => { - expect(syncDonePacket).toEqual(syncPackets) + expect(syncDonePacket).toEqual(syncPackets.length) resolve() }, 1000) } - eventListeners.get("messageReceived")({ detail: syncStep1 }) + getEventListener( + "room", + "messageReceived", + )(createMessageReceivedEvent(syncStep1)) + }), + ) + .then( + () => + new Promise((resolve) => { + // Replay the draw updates + updateRoom.addEventListener( + "addOrUpdatePath", + ({ detail: { id, points } }) => { + updatePaths[id] = points + + clearTimeout(timeout) + timeout = setTimeout(() => resolve(), 1000) + }, + ) + + updateRoom.addEventListener( + "removedIntervalsChange", + ({ detail: { id, intervals } }) => { + updateIntervals[id] = intervals + + clearTimeout(timeout) + timeout = setTimeout(() => resolve(), 1000) + }, + ) + + for (const addPacket of addPackets) { + getEventListener( + "update", + "messageReceived", + )(createMessageReceivedEvent(addPacket)) + } + }), + ) + .then( + () => + new Promise((resolve) => { + // Check the draw updates + expect(updatePaths).toStrictEqual({ [dotID]: dotDraw }) + expect(updateIntervals).toStrictEqual({ [dotID]: {} }) + + resolve() + }), + ) + .then( + () => + new Promise((resolve) => { + // Replay the erase updates + expect(updatePaths).toStrictEqual({ [dotID]: dotDraw }) + expect(updateIntervals).toStrictEqual({ [dotID]: {} }) + + updateRoom.addEventListener( + "removedIntervalsChange", + ({ detail: { id, intervals } }) => { + updateIntervals[id] = intervals + + clearTimeout(timeout) + timeout = setTimeout(() => resolve(), 1000) + }, + ) + + for (const erasePacket of erasePackets) { + getEventListener( + "update", + "messageReceived", + )(createMessageReceivedEvent(erasePacket)) + } + }), + ) + .then( + () => + new Promise((resolve) => { + // Check the erase updates + expect(updatePaths).toStrictEqual({ [dotID]: dotDraw }) + expect(updateIntervals).toStrictEqual({ + [dotID]: { [dotErase[0][0]]: dotErase[0][1] }, + }) + + resolve() + }), + ) + .then( + () => + new Promise((resolve) => { + // Replay the synchronisation + syncRoom.addEventListener( + "addOrUpdatePath", + ({ detail: { id, points } }) => { + syncPaths[id] = points + + clearTimeout(timeout) + timeout = setTimeout(() => resolve(), 1000) + }, + ) + + syncRoom.addEventListener( + "removedIntervalsChange", + ({ detail: { id, intervals } }) => { + syncIntervals[id] = intervals + + clearTimeout(timeout) + timeout = setTimeout(() => resolve(), 1000) + }, + ) + + for (const syncPacket of syncPackets) { + getEventListener( + "sync", + "messageReceived", + )(createMessageReceivedEvent(syncPacket)) + } + }), + ) + .then( + () => + new Promise((resolve) => { + // Check the synchronisation + expect(syncPaths).toStrictEqual({ [dotID]: dotDraw }) + expect(syncIntervals).toStrictEqual({ + [dotID]: { [dotErase[0][0]]: dotErase[0][1] }, + }) + + resolve() }), ) }) @@ -443,7 +620,10 @@ describe("drawing app mesh", () => { prevTime = process.hrtime() - eventListeners.get("messageReceived")({ detail: syncStep1 }) + getEventListener( + "room", + "messageReceived", + )(createMessageReceivedEvent(syncStep1)) }), ) }) @@ -569,7 +749,10 @@ describe("drawing app mesh", () => { prevTime = process.hrtime() - eventListeners.get("messageReceived")({ detail: syncStep1 }) + getEventListener( + "room", + "messageReceived", + )(createMessageReceivedEvent(syncStep1)) }), ) }) @@ -620,16 +803,16 @@ describe("drawing app mesh", () => { () => new Promise((resolve) => { sendListener.callback = (uid, channel, message) => { - expect(uid).toEqual("tiger") + expect(uid).toEqual("moritz") expect(channel).toEqual("y-js") expect(message.message instanceof Uint8Array).toBe(true) syncPackets += 1 if ( - message.message.length == syncDone.message.message.length && + message.message.length == syncDone.message.length && JSON.stringify(Object.assign(message, { uuid: undefined })) == - JSON.stringify(syncDone.message) + JSON.stringify(syncDone) ) { expect(syncDonePacket).toEqual(-1) @@ -644,7 +827,10 @@ describe("drawing app mesh", () => { }, 1000) } - eventListeners.get("messageReceived")({ detail: syncStep1 }) + getEventListener( + "room", + "messageReceived", + )(createMessageReceivedEvent(syncStep1)) }), ) }) diff --git a/package-lock.json b/package-lock.json index 09644afd57c11daafb2df572e15af4710115ac2d..2417731f8da044be4423a51efbbefab9d595adff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -928,9 +928,9 @@ }, "dependencies": { "acorn": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", - "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", "dev": true } } @@ -1251,9 +1251,9 @@ "dev": true }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.0.tgz", + "integrity": "sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==", "dev": true }, "babel-code-frame": { @@ -8698,9 +8698,9 @@ "dev": true }, "resolve": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.0.tgz", - "integrity": "sha512-HHZ3hmOrk5SvybTb18xq4Ek2uLqLO5/goFCYUyvn26nWox4hdlKlfC/+dChIZ6qc4ZeYcN9ekTz0yyHsFgumMw==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.1.tgz", + "integrity": "sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==", "dev": true, "requires": { "path-parse": "^1.0.6" @@ -11004,9 +11004,9 @@ }, "dependencies": { "acorn": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", - "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", "dev": true }, "eslint-scope": { @@ -11043,9 +11043,9 @@ }, "dependencies": { "acorn": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", - "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", "dev": true }, "chalk": { @@ -11424,6 +11424,12 @@ "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", "dev": true }, + "yaeti": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-1.0.2.tgz", + "integrity": "sha512-sc1JByruVRqL6GYdIKbcvYw8PRmYeuwtSd376fM13DNE+JjBh37qIlKjCtqg9mKV2N2+xCfyil3Hd6BXN9W1uQ==", + "dev": true + }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 6e50ba9cb172bf6207fdd7d9948da3fd221d739f..0d23c135e1320c1d063a58623d00563ae51c7884 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,8 @@ "webpack-bundle-analyzer": "^3.6.0", "webpack-cli": "^3.3.9", "webpack-merge": "^4.2.2", - "webpack-preprocessor-loader": "^1.1.2" + "webpack-preprocessor-loader": "^1.1.2", + "yaeti": "^1.0.2" }, "pre-commit": [ "lint", diff --git a/src/app.js b/src/app.js index 968d9b0315a47774ba310ac4bcd5fc54c3a17ce0..57f9c36c3389c68657532106b4f80928a9a8be7b 100644 --- a/src/app.js +++ b/src/app.js @@ -5,7 +5,7 @@ import * as canvas from "./canvas.js" import * as HTML from "./elements.js" -import { computeErasureIntervals, combineErasureIntervals } from "./erasure.js" +import { computeErasureIntervals } from "./erasure.js" import { connect } from "./room.js" import WebRTCConnection from "./connection/WebRTC.js" import * as toolSelection from "./tool-selection.js" @@ -40,26 +40,10 @@ const humanHasher = new humanhash() function eraseEverythingAtPosition(x, y, radius, room) { const mousePos = [x, y] room.getPaths().forEach((points, pathID) => { - const prevPathIntervals = - (room.erasureIntervals || { [pathID]: {} })[pathID] || {} - const newPathIntervals = computeErasureIntervals( - points, - mousePos, - radius, - prevPathIntervals, - ) - - const erasureIntervalsForPath = combineErasureIntervals( - prevPathIntervals, - newPathIntervals, - ) + const newPathIntervals = computeErasureIntervals(points, mousePos, radius) - Object.keys(erasureIntervalsForPath).forEach((pointID) => - room.extendErasureIntervals( - pathID, - pointID, - erasureIntervalsForPath[pointID], - ), + Object.keys(newPathIntervals).forEach((pointID) => + room.extendErasureIntervals(pathID, pointID, newPathIntervals[pointID]), ) }) } @@ -146,17 +130,13 @@ const onRoomConnect = (room_) => { }) room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => { - canvas.renderPath(id, points, room.erasureIntervals[id] || []) + canvas.renderPath(id, points, room.getErasureIntervals(id)) }) room.addEventListener( "removedIntervalsChange", - ({ detail: { id, intervals, points } }) => { - room.erasureIntervals[id] = combineErasureIntervals( - room.erasureIntervals[id] || {}, - intervals, - ) - canvas.renderPath(id, points, room.erasureIntervals[id] || []) + ({ detail: { id, intervals } }) => { + canvas.renderPath(id, room.getPathPoints(id), intervals) }, ) } diff --git a/src/connection/MockConnection.js b/src/connection/MockConnection.js index edc563fe116c1738f5e7d98c1ee5796bb8ffef58..2fa9825675d0f5e13fe8d0be23d871fed665fb25 100644 --- a/src/connection/MockConnection.js +++ b/src/connection/MockConnection.js @@ -1,4 +1,4 @@ -export const getUserID = jest.fn(() => "moritz") +export const getUserID = jest.fn((uid) => uid) export const getPeerHandle = jest.fn((/*uid*/) => undefined) export const getPeerFootprint = jest.fn((/*uid*/) => Promise.resolve(Date.now())) @@ -30,9 +30,11 @@ broadcast.mockClear = () => { export const terminatePeer = jest.fn() export const destructor = jest.fn(() => eventListeners.clear()) -export const eventListeners = new Map() -export const addEventListener = jest.fn((event, callback) => - eventListeners.set(event, callback), +const eventListeners = new Map() +export const getEventListener = (room, event) => + eventListeners.get(`${room}:${event}`) +export const addEventListener = jest.fn((room, event, callback) => + eventListeners.set(`${room}:${event}`, callback), ) const addEventListenerMockClear = addEventListener.mockClear addEventListener.mockClear = () => { @@ -40,22 +42,24 @@ addEventListener.mockClear = () => { addEventListenerMockClear() } -const MockConnection = jest.fn().mockImplementation((/*options*/) => { +const MockConnection = jest.fn().mockImplementation(({ room }) => { setTimeout( () => - eventListeners.has("roomJoined") && eventListeners.get("roomJoined")(), + getEventListener(room, "roomJoined") && + getEventListener(room, "roomJoined")(), 0, ) return { - getUserID, + getUserID: () => getUserID(room), getPeerHandle, getPeerFootprint, send, broadcast, terminatePeer, destructor, - addEventListener, + addEventListener: (event, callback) => + addEventListener(room, event, callback), } }) diff --git a/src/room.js b/src/room.js index ca330db4421a55d8e7940ffb2dd5cd9424e40cf4..69d11a2697c9b8916efbd2abf9d246cf6b615a56 100644 --- a/src/room.js +++ b/src/room.js @@ -16,13 +16,17 @@ 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.erasureIntervals = {} } disconnect() { @@ -58,6 +62,14 @@ class Room extends EventTarget { return paths } + getErasureIntervals(pathID) { + return this._generateRemovedIntervals(pathID) + } + + getPathPoints(pathID) { + return this._generatePath(pathID) + } + get shared() { return this._y.share } @@ -148,8 +160,7 @@ class Room extends EventTarget { const dispatchRemovedIntervalsEvent = (lineEvent) => { const id = lineEvent.name const intervals = this._generateRemovedIntervals(id) - const points = this._generatePath(id) - const detail = { id, intervals, points } + const detail = { id, intervals } this.dispatchEvent( new CustomEvent("removedIntervalsChange", { detail,