Compare commits

..

2 Commits

7 changed files with 711 additions and 37 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

View File

@@ -1,37 +1,28 @@
# P2P Poll App
There are lots of trust-issues:
The possiblity to generate lots of users that do a lot of things (at a rather low cost)
The possibility to put out wrong data, maby not even contradicting but additional to existing data.
The possibility to do all kinds of shenenigans like spam other users with some requests
Due to low programming knowledge, the starting point of this proposal was to mirror how normal groups of people solve issues of trust to then automate and possibly improve the process. There are already some systems out there like Trust flow or random walk.As far as i understand it, the Flexible Trust Web also already does something like this, also maby RWOT and GNUweb but i didn't read into them too much yet since i discovered them rather late.
## Install npm packages
```
npm init -y
npm install
```
If random new people should be able to use the system as equals to previous users, but the system never has real identities as an input, then there is no way to fully prevent the creation of new users to manipulate or sabotage the poll. But it can be assumed, that your friends are rather trustworthy and most likely also their friends and so on. And if someone makes huge ammounts or just one second account, they will probably only have the creator or maby some other people as friends, and even they might already be less socially connected than a normal user.
So the social distance to another user should be evaluated to see, whether you should count their vote.
This is evaluated for and by every user individually, based on the information they were sent. The ammount of contacts you won't count are displayed to you, such that you get a hint at how many people you are missing but also how many people are not counting you. This encourages people to try to prove others/vise versa and make social connections to officially tie the network closer together such that the voting system works and confirms itself. It would be great, if there was some chat attached to the poll. If people want to prove their (or others) trustworhiness within this system, they are then also encouraged to have productive discussions, probably about the matter of the poll.
Everyone in a poll with you is a "contact" of yours.
"users" can have "friends".
You can also manually mark users as suspicious or trustworthy or normal again.
The system for evaluating the trustworthyness of users is somehow a mix between the concepts "weighted path score" and "trust flow" with 5 steps.
That means for 5 steps starting with you, all friends and trusted people of people looked at in this step get some trust from the people we look at: 0.8 * The trust of the looked at person (if trusted) + 0.8 * The trust of the looked at person / friends the looked at person has (if friend). Then the trust of the person that received trust may maximally be 100. The Trust you have to yourself is 100.
You can also mark someone as trustworthy or untrustworthy. That is then also sent around to everyone if you want(should be the standard, but maby a user wants to just see how the trustworthyness will look like after the change).
If you receive such an information, you can make the following calculations immidiately and after every assesment of everyones trustworthyness:
If the accused is less trustworthy then the accusing person, decrease the accused trustworthyness to 0 and the accused friends and trustees trustworthyness by the trustworthyness of the accusing person.
If the trustworhyness of the accusing person is less than the trustworthyness of the accused, then reduce the trustworthyness of the accusing person to 0 and the accusing persons friends and trustees by the trustworthyness of the accused.
If you mark someone as trustworthy:
The Trust flowing to the trusted person from you will also be 0.8 of your trust.
Maby this should also be the effect of beeing "friends" since "trust" might be something you could more intuitively casually deal out after a short chat. If that change were to occur, then the effect would have to be switched around.
All contacts can maximally have the Trust 100.
Note: the frontend obtains the packages `yjs` and `y-websocket` dynamically.
## Run backend
```
node backend.js
```
Future matters:
If there can be any discrepancy of sent information, depending on what sender you trust most, you will mark one of the senders as untrustworthy and neglect all future information from this user. Since everything can be signed and such, that shouldnˋt be an issue tho, but if it was, the ammount of "useless" messages to already informed people might have to increase to validate received data.
A system to showcase the social connections in a 2D - format would be neat.
(most likely something like this exists already)
Obviously the user would also have to see other context like the total of all votes (trusted or not)
### Note on Yjs suggestion
Note that the following server setup is suggested in the Yjs docs (https://docs.yjs.dev/ecosystem/connection-provider/y-websocket):
```
npm install y-websocket-server
HOST=localhost PORT=1000 npx y-websocket
```
However, across a range of npm versions, this does not work.
Nevertheless, the essential code from the `y-websocket-server` package is used here as provided in `utils.js`.
## Run frontend
Open "frontend.html" in browser.
Anonymous polls:
A system of individually assigned trust poses a challenge for a system where you can decide not to trust some voters.
If there is no other option some compromises might be makable, such as:
-Your Friends can know what you voted for
-The Person initiating a poll just decides on the validity of participants according to an own judgement of trust at the moment of poll-creation

31
backend.js Normal file
View File

@@ -0,0 +1,31 @@
import { WebSocketServer } from 'ws';
import { setupWSConnection } from './utils.js'
// Create a WebSocket server
const WS_PORT = 8080;
const wss = new WebSocketServer({ port: WS_PORT });
console.log('WebSocket server is running on ws://localhost:' + String(WS_PORT));
// Connection event handler
wss.on('connection', setupWSConnection);
/*
wss.on('connection', (ws) => {
console.log('Client connected');
// Message event handler
ws.on('message', (message) => {
let msg_str = String(message);
console.log("Received: " + msg_str);
// If this is a text or state message (no Yjs logic) - echo the message back to the client
if (msg_str.startsWith("TEXT_MESSAGE") | msg_str.startsWith("STATE_MESSAGE")) {
ws.send(msg_str);
}
});
// Close event handler
ws.on('close', () => {
console.log('Client disconnected');
});
});*/

196
frontend.html Normal file
View File

@@ -0,0 +1,196 @@
<!DOCTYPE html>
<html>
<head>
<title>Poll Client</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
#messages {
height: 300px;
border: 1px solid #ccc;
overflow-y: auto;
padding: 10px;
margin-bottom: 10px;
}
.message { margin: 5px 0; }
</style>
</head>
<body>
<h1>Poll Client</h1>
<div id="status">Connecting to server...</div>
<div id="status2">Connecting to server...</div>
<div id="messages"></div>
<div>
<input type="text" id="messageInput" placeholder="Type your message">
<!--button onclick="sendMessage()">Send</button-->
<button id="sendBtn">Send</button>
<input id="optionInput" placeholder="Add option">
<button id="addBtn">Add</button>
<ul id="options"></ul>
</div>
<script type="module">
import * as Y from "https://esm.sh/yjs"
import { WebsocketProvider } from "https://esm.sh/y-websocket"
const WS_PORT = 8080;
const ydoc = new Y.Doc();
const status = document.getElementById('status');
const status2 = document.getElementById('status2');
const messages = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
// Connect to the backend (WebSocket server) via Yjs WebsocketProvider
const wsp = new WebsocketProvider(
'ws://localhost:' + String(WS_PORT), 'poll-room',
ydoc
)
wsp.on('status', event => {
//console.log("event.status =", event.status)
if (event.status == "connected") {
status.textContent = 'Yjs connected to WebSocket server';
status.style.color = 'green';
}
else {
status.textContent = 'Yjs disonnected from WebSocket server';
status.style.color = 'red';
}
})
wsp.onopen = () => {
console.log('connected to y-websocket server');
};
ydoc.on('update', () => {
console.log('Yjs document updated locally');
});
wsp.on('sync', isSynced => {
console.log("isSynced =", isSynced)
})
// Connect to the backend (WebSocket server) via common WebSocket (only for informative messages)
const ws = new WebSocket('ws://localhost:' + String(WS_PORT));
ws.onopen = () => {
status2.textContent = 'WebSocket client connected to WebSocket server';
status2.style.color = 'green';
};
ws.onmessage = (event) => {
const message = document.createElement('div');
message.className = 'message';
message.textContent = event.data;
messages.appendChild(message);
messages.scrollTop = messages.scrollHeight;
};
ws.onerror = (error) => {
status2.textContent = 'Error: ' + error.message;
status2.style.color = 'red';
};
ws.onclose = () => {
status2.textContent = 'WebSocket client disconnected from WebSocket server';
status2.style.color = 'red';
};
// Function to send a message
function sendMessage(command, payload) {
let message = "";
if (command === "TEXT_MESSAGE") {
message = command + ":" + messageInput.value.trim();
messageInput.value = "";
}
else if (command.startsWith("STATE_MESSAGE--")) {
message = command + ":" + payload;
}
else {
console.log("Error: unknown command '" + command + "'")
}
if (message) {
/* To make compatible with y-websocket-server API:
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageTextMessage)
syncProtocol.writeUpdate(encoder, update)
enc_message = encoding.toUint8Array(encoder)*/
ws.send(message);
}
}
// Send message on button click
document.getElementById("sendBtn").onclick = () => {
sendMessage("TEXT_MESSAGE", "");
};
// Send message on Enter key
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage("TEXT_MESSAGE", "");
}
});
// Actual poll logic
const optionsMap = ydoc.getMap("options");
const optionsList = document.getElementById("options");
const input = document.getElementById("optionInput");
function render() {
optionsList.innerHTML = ""
optionsMap.forEach((vote, name) => {
// List element for this option
const li = document.createElement("li")
// Label for this option
const label = document.createElement("span")
label.textContent = name + " — " + vote + " "
// "Vote"/"Unvote" button
const voteBtn = document.createElement("button")
if (vote == false) {
voteBtn.textContent = "Vote"
}
else {
voteBtn.textContent = "Unvote"
}
voteBtn.onclick = () => {
vote = !vote;
optionsMap.set(name, vote);
sendMessage("STATE_MESSAGE--" + (vote ? "VOTE" : "UNVOTE"), name);
}
// "Remove" button
const removeBtn = document.createElement("button")
removeBtn.textContent = "Remove"
removeBtn.onclick = () => {
optionsMap.delete(name);
sendMessage("STATE_MESSAGE--REMOVE", name);
}
li.appendChild(label)
li.appendChild(voteBtn)
li.appendChild(removeBtn)
optionsList.appendChild(li)
})
};
document.getElementById("addBtn").onclick = () => {
const name = input.value.trim()
if (!name) return
if (!optionsMap.has(name)) {
optionsMap.set(name, false);
sendMessage("STATE_MESSAGE--ADD_OPTION", name);
}
input.value = ""
};
optionsMap.observe(render);
render();
</script>
</body>
</html>

144
package-lock.json generated Normal file
View File

@@ -0,0 +1,144 @@
{
"name": "yjs-poll",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "yjs-poll",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@y/protocols": "^1.0.6-1",
"lib0": "^0.2.102",
"ws": "^8.19.0",
"yjs": "^14.0.0-7"
}
},
"node_modules/@y/protocols": {
"version": "1.0.6-rc.1",
"resolved": "https://registry.npmjs.org/@y/protocols/-/protocols-1.0.6-rc.1.tgz",
"integrity": "sha512-e/qs7hXcLk/SeNitxMXv2ymozyWFTULwbJEi7cAf/K/iXw9nGwGXHrR5TNluQ/bMwOX1cwuUT0hjEojkfH0gsA==",
"license": "MIT",
"dependencies": {
"lib0": "^1.0.0-rc.1"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
},
"peerDependencies": {
"@y/y": "*"
}
},
"node_modules/@y/protocols/node_modules/lib0": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-1.0.0-rc.4.tgz",
"integrity": "sha512-mESL4089ji2HbS19DwAsYJRrtxZDjzMjEzjNR5kpzFBqlhvV0b7F9+SlA2SHOQU1Puu2A9xejKXtElK77swxLA==",
"license": "MIT",
"bin": {
"0ecdsa-generate-keypair": "src/bin/0ecdsa-generate-keypair.js",
"0gentesthtml": "src/bin/gentesthtml.js",
"0serve": "src/bin/0serve.js"
},
"engines": {
"node": ">=16"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/@y/y": {
"version": "14.0.0-16",
"resolved": "https://registry.npmjs.org/@y/y/-/y-14.0.0-16.tgz",
"integrity": "sha512-4zwbLnLannzUiEdJn9r3IS2FCVdHqADZHLqAivzAuKeoCGZ55JUFebp/YUtrYuTE2ZaBgctxpzulqAAHaahtIA==",
"license": "MIT",
"peer": true,
"dependencies": {
"lib0": "^0.2.115-6"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/isomorphic.js": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT",
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/lib0": {
"version": "0.2.117",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz",
"integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==",
"license": "MIT",
"dependencies": {
"isomorphic.js": "^0.2.4"
},
"bin": {
"0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
"0gentesthtml": "bin/gentesthtml.js",
"0serve": "bin/0serve.js"
},
"engines": {
"node": ">=16"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yjs": {
"version": "14.0.0-16",
"resolved": "https://registry.npmjs.org/yjs/-/yjs-14.0.0-16.tgz",
"integrity": "sha512-n7jMrQz4pgU/NFnf4qY53K2adR/fu6ViQ79qVIw6Og+BtuDs1hx3DjOi3iREVnA6tsxQXXVG3gvG0I2kpmAwoQ==",
"license": "MIT",
"dependencies": {
"lib0": "^0.2.115-6"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
}
}
}

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "yjs-poll",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"y-websocket": "y-websocket"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"ws": "^8.19.0",
"@y/protocols": "^1.0.6-1",
"lib0": "^0.2.102",
"yjs": "^14.0.0-7"
}
}

291
utils.js Normal file
View File

@@ -0,0 +1,291 @@
/* Adapted from https://github.com/yjs/y-websocket-server/blob/main/src/server.js, version of 2025-04-02
(author: dmonad/Kevin Jahns, MIT license) */
import * as Y from 'yjs'
import * as syncProtocol from '@y/protocols/sync'
import * as awarenessProtocol from '@y/protocols/awareness'
import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding'
import * as map from 'lib0/map'
import * as eventloop from 'lib0/eventloop'
/*import { callbackHandler, isCallbackSet } from './callback.js'
const CALLBACK_DEBOUNCE_WAIT = parseInt(process.env.CALLBACK_DEBOUNCE_WAIT || '2000')
const CALLBACK_DEBOUNCE_MAXWAIT = parseInt(process.env.CALLBACK_DEBOUNCE_MAXWAIT || '10000')
const debouncer = eventloop.createDebouncer(CALLBACK_DEBOUNCE_WAIT, CALLBACK_DEBOUNCE_MAXWAIT)*/
const wsReadyStateConnecting = 0
const wsReadyStateOpen = 1
const wsReadyStateClosing = 2 // eslint-disable-line
const wsReadyStateClosed = 3 // eslint-disable-line
// disable gc when using snapshots!
const gcEnabled = process.env.GC !== 'false' && process.env.GC !== '0'
// const persistenceDir = process.env.YPERSISTENCE
/**
* @type {{bindState: function(string,WSSharedDoc):void, writeState:function(string,WSSharedDoc):Promise<any>, provider: any}|null}
*/
let persistence = null
/**
* @param {{bindState: function(string,WSSharedDoc):void,
* writeState:function(string,WSSharedDoc):Promise<any>,provider:any}|null} persistence_
*/
export const setPersistence = persistence_ => {
persistence = persistence_
}
/**
* @return {null|{bindState: function(string,WSSharedDoc):void,
* writeState:function(string,WSSharedDoc):Promise<any>}|null} used persistence layer
*/
export const getPersistence = () => persistence
/**
* @type {Map<string,WSSharedDoc>}
*/
export const docs = new Map()
const messageSync = 0
const messageAwareness = 1
// const messageAuth = 2
//const messageTextMessage = 3
//const messageStateMessage = 4
/**
* @param {Uint8Array} update
* @param {any} _origin
* @param {WSSharedDoc} doc
* @param {any} _tr
*/
const updateHandler = (update, _origin, doc, _tr) => {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageSync)
syncProtocol.writeUpdate(encoder, update)
const message = encoding.toUint8Array(encoder)
doc.conns.forEach((_, conn) => send(doc, conn, message))
}
/**
* @type {(ydoc: Y.Doc) => Promise<void>}
*/
let contentInitializor = _ydoc => Promise.resolve()
/**
* This function is called once every time a Yjs document is created. You can
* use it to pull data from an external source or initialize content.
*
* @param {(ydoc: Y.Doc) => Promise<void>} f
*/
export const setContentInitializor = (f) => {
contentInitializor = f
}
export class WSSharedDoc extends Y.Doc {
/**
* @param {string} name
*/
constructor (name) {
super({ gc: gcEnabled })
this.name = name
/**
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
* @type {Map<Object, Set<number>>}
*/
this.conns = new Map()
/**
* @type {awarenessProtocol.Awareness}
*/
this.awareness = new awarenessProtocol.Awareness(this)
this.awareness.setLocalState(null)
/**
* @param {{ added: Array<number>, updated: Array<number>, removed: Array<number> }} changes
* @param {Object | null} conn Origin is the connection that made the change
*/
const awarenessChangeHandler = ({ added, updated, removed }, conn) => {
const changedClients = added.concat(updated, removed)
if (conn !== null) {
const connControlledIDs = /** @type {Set<number>} */ (this.conns.get(conn))
if (connControlledIDs !== undefined) {
added.forEach(clientID => { connControlledIDs.add(clientID) })
removed.forEach(clientID => { connControlledIDs.delete(clientID) })
}
}
// broadcast awareness update
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageAwareness)
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients))
const buff = encoding.toUint8Array(encoder)
this.conns.forEach((_, c) => {
send(this, c, buff)
})
}
this.awareness.on('update', awarenessChangeHandler)
this.on('update', /** @type {any} */ (updateHandler))
//if (isCallbackSet) {
// this.on('update', (_update, _origin, doc) => {
// debouncer(() => callbackHandler(/** @type {WSSharedDoc} */ (doc)))
// })
//}
this.whenInitialized = contentInitializor(this)
}
}
/**
* Gets a Y.Doc by name, whether in memory or on disk
*
* @param {string} docname - the name of the Y.Doc to find or create
* @param {boolean} gc - whether to allow gc on the doc (applies only when created)
* @return {WSSharedDoc}
*/
export const getYDoc = (docname, gc = true) => map.setIfUndefined(docs, docname, () => {
const doc = new WSSharedDoc(docname)
doc.gc = gc
if (persistence !== null) {
persistence.bindState(docname, doc)
}
docs.set(docname, doc)
return doc
})
/**
* @param {any} conn
* @param {WSSharedDoc} doc
* @param {Uint8Array} message
*/
const messageListener = (conn, doc, message) => {
try {
const encoder = encoding.createEncoder()
const decoder = decoding.createDecoder(new Uint8Array(message))
const messageType = decoding.readVarUint(decoder)
switch (messageType) {
case messageSync:
encoding.writeVarUint(encoder, messageSync)
syncProtocol.readSyncMessage(decoder, encoder, doc, conn)
// If the `encoder` only contains the type of reply message and no
// message, there is no need to send the message. When `encoder` only
// contains the type of reply, its length is 1.
if (encoding.length(encoder) > 1) {
send(doc, conn, encoding.toUint8Array(encoder))
}
break
case messageAwareness: {
awarenessProtocol.applyAwarenessUpdate(doc.awareness, decoding.readVarUint8Array(decoder), conn)
break
}
// If this is a text or a message (no Yjs logic) - echo the message back to the client
default: {
conn.send(String(message))
console.log(String(message))
break
}
}
} catch (err) {
console.error(err)
// @ts-ignore
doc.emit('error', [err])
}
}
/**
* @param {WSSharedDoc} doc
* @param {any} conn
*/
const closeConn = (doc, conn) => {
if (doc.conns.has(conn)) {
/**
* @type {Set<number>}
*/
// @ts-ignore
const controlledIds = doc.conns.get(conn)
doc.conns.delete(conn)
awarenessProtocol.removeAwarenessStates(doc.awareness, Array.from(controlledIds), null)
if (doc.conns.size === 0 && persistence !== null) {
// if persisted, we store state and destroy ydocument
persistence.writeState(doc.name, doc).then(() => {
doc.destroy()
})
docs.delete(doc.name)
}
}
conn.close()
}
/**
* @param {WSSharedDoc} doc
* @param {import('ws').WebSocket} conn
* @param {Uint8Array} m
*/
const send = (doc, conn, m) => {
if (conn.readyState !== wsReadyStateConnecting && conn.readyState !== wsReadyStateOpen) {
closeConn(doc, conn)
}
try {
conn.send(m, {}, err => { err != null && closeConn(doc, conn) })
} catch (e) {
closeConn(doc, conn)
}
}
const pingTimeout = 30000
/**
* @param {import('ws').WebSocket} conn
* @param {import('http').IncomingMessage} req
* @param {any} opts
*/
export const setupWSConnection = (conn, req, { docName = (req.url || '').slice(1).split('?')[0], gc = true } = {}) => {
conn.binaryType = 'arraybuffer'
// get doc, initialize if it does not exist yet
const doc = getYDoc(docName, gc)
doc.conns.set(conn, new Set())
// listen and reply to events
conn.on('message', /** @param {ArrayBuffer} message */ message => messageListener(conn, doc, message))
// Check if connection is still alive
let pongReceived = true
const pingInterval = setInterval(() => {
if (!pongReceived) {
if (doc.conns.has(conn)) {
closeConn(doc, conn)
}
clearInterval(pingInterval)
} else if (doc.conns.has(conn)) {
pongReceived = false
try {
conn.ping()
} catch (e) {
closeConn(doc, conn)
clearInterval(pingInterval)
}
}
}, pingTimeout)
conn.on('close', () => {
closeConn(doc, conn)
clearInterval(pingInterval)
})
conn.on('pong', () => {
pongReceived = true
})
// put the following in a variables in a block so the interval handlers don't keep in in
// scope
{
// send sync step 1
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageSync)
syncProtocol.writeSyncStep1(encoder, doc)
send(doc, conn, encoding.toUint8Array(encoder))
const awarenessStates = doc.awareness.getStates()
if (awarenessStates.size > 0) {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageAwareness)
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())))
send(doc, conn, encoding.toUint8Array(encoder))
}
}
}