|
| 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 | +} |
0 commit comments