diff --git a/.gitignore b/.gitignore index e5ede82..ac37271 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /syncv3 +node_modules diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..7c88118 --- /dev/null +++ b/client/README.md @@ -0,0 +1,13 @@ +## Sliding Sync Client + +![Client](./client.png) + +This is a basic JS client for Sliding Sync. It is designed to be easily readable and is aimed at +developers who want to implement Sliding Sync into their clients. The client is broken up into: + +- `devtools.js` : Code for showing developer statistics. +- `index.js` : Entry point for the client and hooks into the DOM. +- `render.js` : Code to convert data structures into DOM nodes. +- `sync.js` : Sliding Sync code. + +To understand sliding sync, you need to read `index.js` and `sync.js`. The rest of it can be ignored. diff --git a/client/client.png b/client/client.png new file mode 100644 index 0000000..d4a3243 Binary files /dev/null and b/client/client.png differ diff --git a/client/devtools.js b/client/devtools.js index 9ed4ff6..14e322d 100644 --- a/client/devtools.js +++ b/client/devtools.js @@ -1,14 +1,17 @@ // This file contains code to render the developer tools overlay (bandwidth, list visualisations, etc) // You don't need to read this file to understand sliding sync. - /** * Set the bandwidth values on the devtools display. * @param {Element} domNode The node to insert the bandwidth stats to. * @param {SlidingSyncConnection} conn The sliding sync connection */ export function bandwidth(domNode, conn) { - domNode.textContent = (conn.txBytes/1024.0).toFixed(2) + " KB Tx / " + (conn.rxBytes/1024.0).toFixed(2) + " KB Rx"; + domNode.textContent = + (conn.txBytes / 1024.0).toFixed(2) + + " KB Tx / " + + (conn.rxBytes / 1024.0).toFixed(2) + + " KB Rx"; } /** @@ -38,8 +41,9 @@ export function svgify(domNode, activeLists, resp) { if (al.joinedCount > height) { height = al.joinedCount; } - }) - if (height < (window.innerHeight/2)) { // we can double the vertical pixel height to make it easier to see + }); + if (height < window.innerHeight / 2) { + // we can double the vertical pixel height to make it easier to see verticalPixelHeight = 2; } svg.setAttribute("height", height * verticalPixelHeight); @@ -52,34 +56,52 @@ export function svgify(domNode, activeLists, resp) { const colorInvalidate = "#500000"; const colorRoom = "#ffffff"; activeLists.forEach((al, index) => { - const placeholders = document.createElementNS("http://www.w3.org/2000/svg",'rect'); - placeholders.setAttribute("x", index*2*horizontalPixelWidth); + const placeholders = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + placeholders.setAttribute("x", index * 2 * horizontalPixelWidth); placeholders.setAttribute("y", 0); placeholders.setAttribute("width", horizontalPixelWidth); - placeholders.setAttribute("height", al.joinedCount * verticalPixelHeight); - placeholders.setAttribute('fill', colorPlaceholder); + placeholders.setAttribute( + "height", + al.joinedCount * verticalPixelHeight + ); + placeholders.setAttribute("fill", colorPlaceholder); svg.appendChild(placeholders); // [[0, 20], [50,60]]; al.activeRanges.forEach((range) => { - const rect = document.createElementNS("http://www.w3.org/2000/svg",'rect'); - rect.setAttribute('x',index*2*horizontalPixelWidth); - rect.setAttribute('y',range[0]*verticalPixelHeight); - rect.setAttribute('width',horizontalPixelWidth); - rect.setAttribute('height',(range[1]-range[0]) * verticalPixelHeight); - rect.setAttribute('fill',colorInWindow); + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + rect.setAttribute("x", index * 2 * horizontalPixelWidth); + rect.setAttribute("y", range[0] * verticalPixelHeight); + rect.setAttribute("width", horizontalPixelWidth); + rect.setAttribute( + "height", + (range[1] - range[0]) * verticalPixelHeight + ); + rect.setAttribute("fill", colorInWindow); svg.appendChild(rect); }); }); const addLine = (index, y, colour, yLen) => { - const bar = document.createElementNS("http://www.w3.org/2000/svg",'rect'); - bar.setAttribute("x", index*2*horizontalPixelWidth); - bar.setAttribute("y", y*verticalPixelHeight); - bar.setAttribute('width',horizontalPixelWidth); - bar.setAttribute('height',verticalPixelHeight*(yLen?yLen:1)); - bar.setAttribute('fill', colour); - const animation = document.createElementNS("http://www.w3.org/2000/svg","animate"); + const bar = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + bar.setAttribute("x", index * 2 * horizontalPixelWidth); + bar.setAttribute("y", y * verticalPixelHeight); + bar.setAttribute("width", horizontalPixelWidth); + bar.setAttribute("height", verticalPixelHeight * (yLen ? yLen : 1)); + bar.setAttribute("fill", colour); + const animation = document.createElementNS( + "http://www.w3.org/2000/svg", + "animate" + ); animation.setAttribute("attributeName", "visibility"); animation.setAttribute("from", "visible"); animation.setAttribute("to", "hidden"); @@ -87,7 +109,7 @@ export function svgify(domNode, activeLists, resp) { animation.setAttribute("repeatCount", "3"); bar.appendChild(animation); svg.appendChild(bar); - } + }; // add insertions, deletions and updates resp.ops.forEach((op) => { @@ -98,24 +120,40 @@ export function svgify(domNode, activeLists, resp) { } else if (op.op === "UPDATE") { addLine(op.list, op.index, colorUpdate); } else if (op.op === "SYNC") { - addLine(op.list, op.range[0], colorSync, op.range[1]-op.range[0]+1); + addLine( + op.list, + op.range[0], + colorSync, + op.range[1] - op.range[0] + 1 + ); } else if (op.op === "INVALIDATE") { - addLine(op.list, op.range[0], colorInvalidate, op.range[1]-op.range[0]+1); + addLine( + op.list, + op.range[0], + colorInvalidate, + op.range[1] - op.range[0] + 1 + ); } }); // this is expensive so only do it on smaller accounts if (height < 500 && false) { - const fifth = horizontalPixelWidth/5; + const fifth = horizontalPixelWidth / 5; // draw white dot for each room which has some kind of data stored activeLists.forEach((al, index) => { for (let roomIndex of Object.keys(al.roomIndexToRoomId)) { - const roomPixel = document.createElementNS("http://www.w3.org/2000/svg",'rect'); - roomPixel.setAttribute("x", index*2*horizontalPixelWidth + fifth); - roomPixel.setAttribute("y", roomIndex*verticalPixelHeight); - roomPixel.setAttribute('width',fifth); - roomPixel.setAttribute('height',verticalPixelHeight); - roomPixel.setAttribute('fill', colorRoom); + const roomPixel = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + roomPixel.setAttribute( + "x", + index * 2 * horizontalPixelWidth + fifth + ); + roomPixel.setAttribute("y", roomIndex * verticalPixelHeight); + roomPixel.setAttribute("width", fifth); + roomPixel.setAttribute("height", verticalPixelHeight); + roomPixel.setAttribute("fill", colorRoom); svg.appendChild(roomPixel); } }); @@ -132,4 +170,4 @@ export function svgify(domNode, activeLists, resp) { // insert the SVG domNode.appendChild(svg); -} \ No newline at end of file +} diff --git a/client/index.js b/client/index.js index 0a16b59..b2f9cb7 100644 --- a/client/index.js +++ b/client/index.js @@ -1,7 +1,7 @@ // This file contains the entry point for the client, as well as DOM interactions. -import { SlidingList, SlidingSyncConnection } from './sync.js'; -import * as render from './render.js'; -import * as devtools from './devtools.js'; +import { SlidingList, SlidingSyncConnection } from "./sync.js"; +import * as render from "./render.js"; +import * as devtools from "./devtools.js"; let activeSessionId; let activeRoomId = ""; // the room currently being viewed @@ -125,14 +125,18 @@ const intersectionObserver = new IntersectionObserver( listIndexToStartEnd[listIndex].endIndex = i; } }); - console.log("Intersection indexes:", JSON.stringify(listIndexToStartEnd)); + console.log( + "Intersection indexes:", + JSON.stringify(listIndexToStartEnd) + ); // buffer range const bufferRange = 5; Object.keys(listIndexToStartEnd).forEach((listIndex) => { let startIndex = listIndexToStartEnd[listIndex].startIndex; let endIndex = listIndexToStartEnd[listIndex].endIndex; - startIndex = startIndex - bufferRange < 0 ? 0 : startIndex - bufferRange; + startIndex = + startIndex - bufferRange < 0 ? 0 : startIndex - bufferRange; endIndex = endIndex + bufferRange >= activeLists[listIndex].joinedCount ? activeLists[listIndex].joinedCount - 1 @@ -212,15 +216,20 @@ const renderRoomContent = (roomId, refresh) => { } let room = rooms.roomIdToRoom[activeRoomId]; if (!room) { - console.error("renderRoomContent: unknown active room ID ", activeRoomId); + console.error( + "renderRoomContent: unknown active room ID ", + activeRoomId + ); return; } - document.getElementById("selectedroomname").textContent = room.name || room.room_id; + document.getElementById("selectedroomname").textContent = + room.name || room.room_id; if (room.avatar) { document.getElementById("selectedroomavatar").src = mxcToUrl(room.avatar) || "/client/placeholder.svg"; } else { - document.getElementById("selectedroomavatar").src = "/client/placeholder.svg"; + document.getElementById("selectedroomavatar").src = + "/client/placeholder.svg"; } if (room.topic) { document.getElementById("selectedroomtopic").textContent = room.topic; @@ -270,7 +279,13 @@ const renderList = (container, listIndex) => { addCount += 1; } if (addCount > 0 || removeCount > 0) { - console.log("render: added ", addCount, "nodes, removed", removeCount, "nodes"); + console.log( + "render: added ", + addCount, + "nodes, removed", + removeCount, + "nodes" + ); } // loop all elements and modify the contents for (let i = 0; i < container.children.length; i++) { @@ -278,10 +293,13 @@ const renderList = (container, listIndex) => { const roomId = listData.roomIndexToRoomId[i]; const r = rooms.roomIdToRoom[roomId]; const roomNameSpan = roomCell.getElementsByClassName("roomname")[0]; - const roomContentSpan = roomCell.getElementsByClassName("roomcontent")[0]; + const roomContentSpan = + roomCell.getElementsByClassName("roomcontent")[0]; const roomSenderSpan = roomCell.getElementsByClassName("roomsender")[0]; - const roomTimestampSpan = roomCell.getElementsByClassName("roomtimestamp")[0]; - const unreadCountSpan = roomCell.getElementsByClassName("unreadcount")[0]; + const roomTimestampSpan = + roomCell.getElementsByClassName("roomtimestamp")[0]; + const unreadCountSpan = + roomCell.getElementsByClassName("unreadcount")[0]; unreadCountSpan.textContent = ""; unreadCountSpan.classList.remove("unreadcountnotify"); unreadCountSpan.classList.remove("unreadcounthighlight"); @@ -293,7 +311,8 @@ const renderList = (container, listIndex) => { roomContentSpan.style = "background: #e0e0e0; color: #e0e0e0;"; roomSenderSpan.textContent = ""; roomTimestampSpan.textContent = ""; - roomCell.getElementsByClassName("roomavatar")[0].src = "/client/placeholder.svg"; + roomCell.getElementsByClassName("roomavatar")[0].src = + "/client/placeholder.svg"; roomCell.style = ""; continue; } @@ -305,7 +324,8 @@ const renderList = (container, listIndex) => { roomCell.getElementsByClassName("roomavatar")[0].src = mxcToUrl(r.avatar) || "/client/placeholder.svg"; } else { - roomCell.getElementsByClassName("roomavatar")[0].src = "/client/placeholder.svg"; + roomCell.getElementsByClassName("roomavatar")[0].src = + "/client/placeholder.svg"; } if (roomId === activeRoomId) { roomCell.style = "background: #d7d7f7"; @@ -328,7 +348,9 @@ const renderList = (container, listIndex) => { const mostRecentEvent = r.timeline[r.timeline.length - 1]; roomSenderSpan.textContent = mostRecentEvent.sender; // TODO: move to render.js - roomTimestampSpan.textContent = render.formatTimestamp(mostRecentEvent.origin_server_ts); + roomTimestampSpan.textContent = render.formatTimestamp( + mostRecentEvent.origin_server_ts + ); const body = render.textForEvent(mostRecentEvent); if (mostRecentEvent.type === "m.room.member") { @@ -362,7 +384,12 @@ const indexInRange = (listIndex, i) => { }; const doSyncLoop = async (accessToken, sessionId) => { - console.log("Starting sync loop. Active: ", activeSessionId, " this:", sessionId); + console.log( + "Starting sync loop. Active: ", + activeSessionId, + " this:", + sessionId + ); let currentPos; let currentSub = ""; @@ -380,7 +407,11 @@ const doSyncLoop = async (accessToken, sessionId) => { if (!currentPos) { l.required_state = requiredStateEventsInList; l.timeline_limit = 1; - l.sort = ["by_highlight_count", "by_notification_count", "by_recency"]; + l.sort = [ + "by_highlight_count", + "by_notification_count", + "by_recency", + ]; } return l; }), @@ -400,7 +431,11 @@ const doSyncLoop = async (accessToken, sessionId) => { // hold a ref to the active room ID as it may change by the time we return from doSyncRequest subscribingToRoom = activeRoomId; } - resp = await syncConnection.doSyncRequest(accessToken, currentPos, reqBody); + resp = await syncConnection.doSyncRequest( + accessToken, + currentPos, + reqBody + ); currentPos = resp.pos; // update what we think we're subscribed to. if (subscribingToRoom) { @@ -452,7 +487,8 @@ const doSyncLoop = async (accessToken, sessionId) => { // something is in this space, shift items out of the way if (gapIndex < 0) { console.log( - "cannot work out where gap is, INSERT without previous DELETE! List: ", op.list, + "cannot work out where gap is, INSERT without previous DELETE! List: ", + op.list ); return; } @@ -474,7 +510,9 @@ const doSyncLoop = async (accessToken, sessionId) => { for (let i = gapIndex; i > op.index; i--) { if (indexInRange(op.list, i)) { activeLists[op.list].roomIndexToRoomId[i] = - activeLists[op.list].roomIndexToRoomId[i - 1]; + activeLists[op.list].roomIndexToRoomId[ + i - 1 + ]; } } } else if (gapIndex < op.index) { @@ -483,13 +521,19 @@ const doSyncLoop = async (accessToken, sessionId) => { for (let i = gapIndex; i < op.index; i++) { if (indexInRange(op.list, i)) { activeLists[op.list].roomIndexToRoomId[i] = - activeLists[op.list].roomIndexToRoomId[i + 1]; + activeLists[op.list].roomIndexToRoomId[ + i + 1 + ]; } } } } - accumulateRoomData(op.room, rooms.roomIdToRoom[op.room.room_id] !== undefined); - activeLists[op.list].roomIndexToRoomId[op.index] = op.room.room_id; + accumulateRoomData( + op.room, + rooms.roomIdToRoom[op.room.room_id] !== undefined + ); + activeLists[op.list].roomIndexToRoomId[op.index] = + op.room.room_id; renderRoomContent(op.room.room_id); } else if (op.op === "UPDATE") { console.log("UPDATE", op.list, op.index, op.room.room_id, ";"); @@ -507,15 +551,30 @@ const doSyncLoop = async (accessToken, sessionId) => { syncRooms.push(r.room_id); accumulateRoomData(r); } - console.log("SYNC", op.list, op.range[0], op.range[1], syncRooms.join(" "), ";"); + console.log( + "SYNC", + op.list, + op.range[0], + op.range[1], + syncRooms.join(" "), + ";" + ); } else if (op.op === "INVALIDATE") { let invalidRooms = []; const startIndex = op.range[0]; for (let i = startIndex; i <= op.range[1]; i++) { - invalidRooms.push(activeLists[op.list].roomIndexToRoomId[i]); + invalidRooms.push( + activeLists[op.list].roomIndexToRoomId[i] + ); delete activeLists[op.list].roomIndexToRoomId[i]; } - console.log("INVALIDATE", op.list, op.range[0], op.range[1], ";"); + console.log( + "INVALIDATE", + op.list, + op.range[0], + op.range[1], + ";" + ); } }); const roomListElements = document.getElementsByClassName("roomlist"); @@ -568,12 +627,21 @@ const doSyncLoop = async (accessToken, sessionId) => { } }); - devtools.svgify(document.getElementById("listgraph"), activeLists, resp); + devtools.svgify( + document.getElementById("listgraph"), + activeLists, + resp + ); } - console.log("active session: ", activeSessionId, " this session: ", sessionId, " terminating."); + console.log( + "active session: ", + activeSessionId, + " this session: ", + sessionId, + " terminating." + ); }; - const randomName = (i, long) => { if (i % 17 === 0) { return long @@ -588,11 +656,17 @@ const randomName = (i, long) => { ? "That’s how it is with people. Nobody cares how it works as long as it works." : "I know kung fu"; } else if (i % 7 === 0) { - return long ? "The body cannot live without the mind." : "Free your mind"; + return long + ? "The body cannot live without the mind." + : "Free your mind"; } else if (i % 5 === 0) { - return long ? "Perhaps we are asking the wrong questions…" : "Agent Smith"; + return long + ? "Perhaps we are asking the wrong questions…" + : "Agent Smith"; } else if (i % 3 === 0) { - return long ? "You've been living in a dream world, Neo." : "Mr Anderson"; + return long + ? "You've been living in a dream world, Neo." + : "Mr Anderson"; } else { return long ? "Mr. Wizard, get me the hell out of here! " : "Morpheus"; } diff --git a/client/render.js b/client/render.js index 5fcdf34..322dcbe 100644 --- a/client/render.js +++ b/client/render.js @@ -23,7 +23,9 @@ const membershipChangeText = (ev) => { if (nowMembership == prevMembership && nowMembership == "join") { // display name or avatar change if (prevContent.displayname !== ev.content.displayname) { - return ev.state_key + " set their name to " + ev.content.displayname; + return ( + ev.state_key + " set their name to " + ev.content.displayname + ); } if (prevContent.avatar_url !== ev.content.avatar_url) { return ev.state_key + " changed their profile picture"; @@ -79,11 +81,9 @@ export const renderEvent = (eventIdKey, ev) => { const msgCell = template.content.firstElementChild.cloneNode(true); msgCell.setAttribute("id", eventIdKey); msgCell.getElementsByClassName("msgsender")[0].textContent = ev.sender; - msgCell.getElementsByClassName("msgtimestamp")[0].textContent = formatTimestamp( - ev.origin_server_ts - ); + msgCell.getElementsByClassName("msgtimestamp")[0].textContent = + formatTimestamp(ev.origin_server_ts); let body = textForEvent(ev); msgCell.getElementsByClassName("msgcontent")[0].textContent = body; return msgCell; -} - +}; diff --git a/client/sync.js b/client/sync.js index 6fe0310..15f99f8 100644 --- a/client/sync.js +++ b/client/sync.js @@ -1,13 +1,12 @@ // This file contains the main sliding sync code. -import * as devtools from './devtools.js'; +import * as devtools from "./devtools.js"; // The default range to /always/ track on a list. // When you scroll the list, new windows are added to the first element. E.g [[0,20], [37,45]] // TODO: explain why const DEFAULT_RANGES = [[0, 20]]; - /** * SlidingSyncConnection is a thin wrapper around fetch() which performs a sliding sync request. * The wrapper persists a small amount of extra data including the total number of tx/rx bytes, @@ -41,15 +40,18 @@ export class SlidingSyncConnection { async doSyncRequest(accessToken, pos, reqBody) { this.abortController = new AbortController(); const jsonBody = JSON.stringify(reqBody); - let resp = await fetch("/_matrix/client/v3/sync" + (pos ? "?pos=" + pos : ""), { - signal: this.abortController.signal, - method: "POST", - headers: { - Authorization: "Bearer " + accessToken, - "Content-Type": "application/json", - }, - body: jsonBody, - }); + let resp = await fetch( + "/_matrix/client/v3/sync" + (pos ? "?pos=" + pos : ""), + { + signal: this.abortController.signal, + method: "POST", + headers: { + Authorization: "Bearer " + accessToken, + "Content-Type": "application/json", + }, + body: jsonBody, + } + ); let respBody = await resp.json(); if (respBody.ops) { @@ -66,7 +68,9 @@ export class SlidingSyncConnection { if (respBody.error) { this.lastError = respBody.error; } - throw new Error("/sync returned HTTP " + resp.status + " " + respBody.error); + throw new Error( + "/sync returned HTTP " + resp.status + " " + respBody.error + ); } this.lastError = null; return respBody; @@ -80,7 +84,7 @@ export class SlidingSyncConnection { export class SlidingList { /** * Construct a new sliding list. - * @param {string} name Human-readable name to display on the UI for this list. + * @param {string} name Human-readable name to display on the UI for this list. * @param {object} filters Optional. The sliding sync filters to apply e.g { is_dm: true }. */ constructor(name, filters) { @@ -119,12 +123,9 @@ export class SlidingList { } class SlidingSync { - /** - * - * @param {[]SlidingList} activeLists + * + * @param {[]SlidingList} activeLists */ - constructor(activeLists) { - - } -} \ No newline at end of file + constructor(activeLists) {} +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ecbb6b6 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "sliding-sync", + "version": "0.1.0", + "private": true, + "devDependencies": { + "prettier": "^2.1.2" + }, + "prettier": { + "trailingComma": "es5", + "tabWidth": 4, + "semi": true, + "singleQuote": false + } +}