Skip to content

Commit 65fafff

Browse files
authored
completed OT client base functionality (#193)
1 parent ca59dd3 commit 65fafff

File tree

13 files changed

+4719
-72
lines changed

13 files changed

+4719
-72
lines changed

backend/client/.eslintrc.js renamed to backend/OTClient/.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module.exports = {
66
extends: [
77
'standard'
88
],
9+
semi: [1, "always"],
910
parser: '@typescript-eslint/parser',
1011
parserOptions: {
1112
ecmaVersion: 'latest',
File renamed without changes.

backend/OTClient/client.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Client server implementation
3+
*/
4+
5+
import { io, Socket } from 'socket.io-client';
6+
import { Operation } from './operation';
7+
import { OperationQueue } from './operationQueue';
8+
import { bind } from './option';
9+
10+
const ACK_TIMEOUT_DURATION = 10_000;
11+
12+
/*
13+
The Client-Server protocol
14+
- The general outline of our client-server protocol is as follows:
15+
- Client wants to send an operation (it applies it locally)
16+
- If there are any operations in the op buffer it pushes it to the end
17+
- If there aren't it sends it directly to the server
18+
19+
- The client then awaits for an acknowledgment
20+
- While it waits of an acknowledgement it queues everything in the buffer
21+
- All incoming operations from the server are transformed against buffer operations (As they haven't been applied yet)
22+
- When at acknowledgement is received the client then sends the next queued operation to the server
23+
*/
24+
25+
export default class Client {
26+
// TODO: Handle destruction / closing of the websocket
27+
constructor (opCallback: (op: Operation) => void) {
28+
this.socket = io(`ws://localhost:8080/edit?document=${document}`);
29+
30+
this.socket.on('connect', this.handleConnection);
31+
this.socket.on('ack', this.handleAck);
32+
this.socket.on('op', this.handleOperation (opCallback));
33+
}
34+
35+
// handleAck handles an incoming acknowledgement operation
36+
private handleAck = () => {
37+
clearTimeout(this.timeoutID);
38+
this.pendingAcknowledgement = false;
39+
40+
// dequeue the current operation and send a new one if required
41+
this.queuedOperations.dequeueOperation();
42+
bind((op) => this.sendToServer(op), this.queuedOperations.peekHead());
43+
}
44+
45+
// handleOperation handles an incoming operation from the server
46+
private handleOperation = (opCallback: (op: Operation) => void) => (operation: Operation) => {
47+
const transformedOp = this.queuedOperations.applyAndTransformIncomingOperation(operation);
48+
opCallback(transformedOp);
49+
50+
this.appliedOperations += 1;
51+
};
52+
53+
// handleConnection handles the even when the connection opens
54+
private handleConnection = () => {
55+
console.log(
56+
`Socket ${this.socket.id} connected: ${this.socket.connected}`
57+
);
58+
};
59+
60+
/**
61+
* Send an operation from client to centralised server through websocket
62+
*
63+
* @param operation the operation the client wants to send
64+
*/
65+
public pushOperation = (operation: Operation) => {
66+
// Note that if there aren't any pending acknowledgements then the operation queue will be empty
67+
this.queuedOperations.enqueueOperation(operation);
68+
69+
if (!this.pendingAcknowledgement) {
70+
this.sendToServer(operation);
71+
}
72+
}
73+
74+
// sendToServer pushes an operation to the server
75+
private sendToServer = (operation: Operation) => {
76+
this.pendingAcknowledgement = true;
77+
78+
this.socket.send(JSON.stringify({ operation, appliedOperations: this.appliedOperations }));
79+
this.timeoutID = setTimeout(() => {
80+
throw new Error(`Did not receive ACK after ${ACK_TIMEOUT_DURATION} ms!`);
81+
},
82+
ACK_TIMEOUT_DURATION,
83+
'finish'
84+
);
85+
}
86+
87+
private socket: Socket;
88+
89+
private queuedOperations: OperationQueue = new OperationQueue();
90+
private pendingAcknowledgement = false;
91+
private appliedOperations = 0;
92+
93+
private timeoutID: number = NaN;
94+
}

backend/OTClient/operation.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface Operation {
2+
operation: string;
3+
index: number;
4+
}
5+
6+
7+
export const transform = (a: Operation, b: Operation): [Operation, Operation] => [{ operation: 'add', index: 3}, { operation: 'add', index: 3}];

backend/OTClient/operationQueue.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Operation, transform } from './operation'
2+
import { None, Option, Some } from './option'
3+
4+
// OperationQueue is a simple data structure of the maintenance of outgoing operations, new operations are pushed to this queue
5+
// and when and incoming operation from the server applies that operation is transformed against all elements of this queue
6+
export class OperationQueue {
7+
8+
// queueOperation pushes an operation to the end of the operation queue
9+
public enqueueOperation = (operation: Operation) =>
10+
this.operationQueue.push(operation);
11+
12+
// applyIncomingOperation takes an incoming operation from the server and applies
13+
// it to all elements of the queue, it returns the serverOp transformed against all operations
14+
// in the operation queue
15+
public applyAndTransformIncomingOperation = (serverOp: Operation): Operation => {
16+
const { newQueue, newOp } =
17+
this.operationQueue.reduce(
18+
(prevSet, op) => {
19+
const newOp = transform(op, prevSet.newOp);
20+
return { newQueue: prevSet.newQueue.concat(newOp), newOp: newOp[1] };
21+
},
22+
{ newQueue: [] as Operation[], newOp: serverOp}
23+
);
24+
25+
this.operationQueue = newQueue;
26+
return newOp;
27+
}
28+
29+
// isEmpty determines if there are any operations queued or not
30+
public isEmpty = (): boolean => this.operationQueue.length === 0;
31+
32+
// dequeueOperation removes the operation at the head of the operation queue
33+
public dequeueOperation = (): Option<Operation> =>
34+
this.isEmpty()
35+
? None()
36+
: Some(this.operationQueue.shift());
37+
38+
// peekHead just peeks at the head of the operation queue
39+
public peekHead = (): Option<Operation> =>
40+
this.isEmpty()
41+
? None()
42+
: Some(this.operationQueue[0]);
43+
44+
// operationQueue is our internal operation queue
45+
private operationQueue = [] as Operation[];
46+
}

backend/OTClient/option.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Type definitions for the option type
2+
export type Option<T> = T | undefined;
3+
export const Some = <T>(x: T) : Option<T> => x;
4+
export const None = <T>() : Option<T> => undefined;
5+
6+
// Optional Bind functions
7+
export const bind = <T, V>(f: (x: T) => V, y: Option<T>): Option<V> =>
8+
y !== undefined
9+
? Some(f(y))
10+
: None();

0 commit comments

Comments
 (0)