client: use prettier; add README

This commit is contained in:
Kegan Dougal 2022-02-23 19:26:38 +00:00
parent 3867cece40
commit 808d7f8d21
8 changed files with 231 additions and 90 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/syncv3
node_modules

13
client/README.md Normal file
View File

@ -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.

BIN
client/client.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

View File

@ -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);
}
});

View File

@ -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) => {
? "Thats 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";
}

View File

@ -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;
}
};

View File

@ -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;
@ -119,12 +123,9 @@ export class SlidingList {
}
class SlidingSync {
/**
*
* @param {[]SlidingList} activeLists
*/
constructor(activeLists) {
}
constructor(activeLists) {}
}

14
package.json Normal file
View File

@ -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
}
}