Skip to content

Commit 827f373

Browse files
authored
Merge pull request #193 from ls1intum/feature/base64-ydoc-sync-over-websocket
feat: Use base64 strings in library functions for yjs syncs
2 parents e57ff25 + b26343c commit 827f373

File tree

6 files changed

+70
-21
lines changed

6 files changed

+70
-21
lines changed

library/lib/apollon-editor.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,12 +269,12 @@ export class ApollonEditor {
269269
}
270270
}
271271

272-
public sendBroadcastMessage(sendFn: (data: Uint8Array) => void) {
272+
public sendBroadcastMessage(sendFn: (base64Data: string) => void) {
273273
this.syncManager.setSendFunction(sendFn)
274274
}
275275

276-
public receiveBroadcastedMessage(update: Uint8Array) {
277-
this.syncManager.handleReceivedData(update)
276+
public receiveBroadcastedMessage(base64Data: string) {
277+
this.syncManager.handleReceivedData(base64Data)
278278
}
279279

280280
public updateDiagramTitle(name: string) {
@@ -312,4 +312,5 @@ export class ApollonEditor {
312312
public addOrUpdateAssessment(assessment: Apollon.Assessment): void {
313313
this.diagramStore.getState().addOrUpdateAssessment(assessment)
314314
}
315+
public uint8ToBase64 = YjsSyncClass.uint8ToBase64
315316
}

library/lib/store/yjsSync.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ enum MessageType {
1616
YjsUpdate = 1,
1717
}
1818

19-
type SendFunction = (data: Uint8Array) => void
19+
type SendFunction = (data: string) => void
2020

2121
export class YjsSyncClass {
2222
private readonly stopYjsObserver: () => void
@@ -44,23 +44,27 @@ export class YjsSyncClass {
4444
this.sendFunction = sendFn
4545
}
4646

47-
public applyUpdate = (update: Uint8Array, transactionOrigin: string) => {
47+
private applyUpdate = (update: Uint8Array, transactionOrigin: string) => {
4848
Y.applyUpdate(this.ydoc, update, transactionOrigin)
4949
}
5050

51-
public handleReceivedData = (data: Uint8Array) => {
52-
const messageType = data[0]
51+
public handleReceivedData = (base64Data: string) => {
52+
// Decode the base64 string to Uint8Array
53+
const decodedData = this.base64ToUint8(base64Data)
54+
const messageType = decodedData[0]
5355

5456
if (messageType === MessageType.YjsUpdate) {
55-
const update = data.slice(1)
57+
const update = decodedData.slice(1)
5658
this.applyUpdate(update, "remote")
5759
} else if (messageType === MessageType.YjsSYNC) {
5860
if (this.sendFunction) {
5961
const syncMessage = Y.encodeStateAsUpdate(this.ydoc)
6062
const fullMessage = new Uint8Array(1 + syncMessage.length)
6163
fullMessage[0] = MessageType.YjsUpdate
6264
fullMessage.set(syncMessage, 1)
63-
this.sendFunction(fullMessage)
65+
66+
const base64Message = YjsSyncClass.uint8ToBase64(fullMessage)
67+
this.sendFunction(base64Message)
6468
}
6569
}
6670
}
@@ -113,7 +117,8 @@ export class YjsSyncClass {
113117
const fullMessage = new Uint8Array(1 + syncMessage.length)
114118
fullMessage[0] = MessageType.YjsUpdate
115119
fullMessage.set(syncMessage, 1)
116-
this.sendFunction(fullMessage)
120+
const base64Message = YjsSyncClass.uint8ToBase64(fullMessage)
121+
this.sendFunction(base64Message)
117122
}
118123
}
119124

@@ -130,4 +135,23 @@ export class YjsSyncClass {
130135
this.ydoc.off("update", handleYjsUpdate)
131136
}
132137
}
138+
139+
/**
140+
* Convert Uint8Array to Base64 string
141+
*/
142+
static uint8ToBase64(uint8: Uint8Array): string {
143+
return btoa(String.fromCharCode(...uint8))
144+
}
145+
146+
/**
147+
* Convert Base64 string to Uint8Array
148+
*/
149+
private base64ToUint8(base64: string): Uint8Array {
150+
const binary = atob(base64)
151+
const bytes = new Uint8Array(binary.length)
152+
for (let i = 0; i < binary.length; i++) {
153+
bytes[i] = binary.charCodeAt(i)
154+
}
155+
return bytes
156+
}
133157
}

standalone/server/src/relaySocketServer.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,22 @@ export const startSocketServer = (): void => {
4444

4545
ws.on("message", (message: WebSocket.RawData) => {
4646
const clients = diagrams.get(ws.diagramId!)
47-
4847
if (!clients) return
4948

49+
// Convert message to string if it's a Buffer or string
50+
// This is necessary because WebSocket messages can be sent as Buffer or string
51+
// and we want to ensure we send a string to all clients
52+
const messageString =
53+
typeof message === "string"
54+
? message
55+
: message instanceof Buffer
56+
? message.toString("utf-8")
57+
: ""
58+
5059
let count = 0
5160
clients.forEach((client) => {
5261
if (client !== ws && client.readyState === WebSocket.OPEN) {
53-
client.send(message)
62+
client.send(messageString)
5463
count++
5564
}
5665
})

standalone/webapp/src/pages/ApollonWithConnection.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import { useNavigate, useParams, useSearchParams } from "react-router"
1111
import { toast } from "react-toastify"
1212
import { backendURL, backendWSSUrl } from "@/constants"
13-
import { DiagramView } from "@/types"
13+
import { DiagramView, WebSocketMessage } from "@/types"
1414

1515
const fetchDiagramData = (diagramId: string): Promise<any> => {
1616
return fetch(`${backendURL}/api/${diagramId}`, {
@@ -112,23 +112,33 @@ export const ApollonWithConnection: React.FC = () => {
112112
)
113113

114114
// Handle incoming Yjs updates
115-
websocketRef.current.onmessage = (event: MessageEvent<Blob>) => {
116-
event.data.arrayBuffer().then((buffer: ArrayBuffer) => {
117-
const data = new Uint8Array(buffer)
118-
instance?.receiveBroadcastedMessage(data)
119-
})
115+
websocketRef.current.onmessage = (event: MessageEvent<string>) => {
116+
const receiveWebSocketMessage = JSON.parse(
117+
event.data
118+
) as WebSocketMessage
119+
instance?.receiveBroadcastedMessage(
120+
receiveWebSocketMessage.diagramData
121+
)
120122
}
121123

122124
// Wait until socket is open before starting sync
123125
websocketRef.current.onopen = () => {
124-
instance?.sendBroadcastMessage((data) => {
126+
instance?.sendBroadcastMessage((diagramData) => {
125127
if (websocketRef.current?.readyState === WebSocket.OPEN) {
126-
websocketRef.current.send(data)
128+
const sendData = { diagramData }
129+
websocketRef.current.send(JSON.stringify(sendData))
127130
} else {
128131
console.warn("Tried to send while WebSocket not open")
129132
}
130133
})
131-
websocketRef.current?.send(new Uint8Array([0])) // Init message
134+
135+
const initialSyncMessageInUintArray = instance?.uint8ToBase64(
136+
new Uint8Array([0])
137+
)
138+
const initialMessage = JSON.stringify({
139+
diagramData: initialSyncMessageInUintArray,
140+
})
141+
websocketRef.current?.send(initialMessage)
132142
}
133143

134144
websocketRef.current.onerror = (err) => {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type WebSocketMessage = {
2+
// new fields can be added like collaborators name-color, etc.
3+
diagramData: string
4+
}

standalone/webapp/src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "./ModalTypes"
2+
export * from "./WebSocketMessage"

0 commit comments

Comments
 (0)