76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
import { ExponentialBackoff, Websocket, WebsocketBuilder } from 'websocket-ts'
|
|
import { ActionClientMessage, ActionServerMessage, ConfirmServerMessage, HandshakeClientMessage, HandshakeServerMessage, ServerMessage, KetchupSynced } from 'ketchup-common'
|
|
import EventEmitter from 'events'
|
|
|
|
export default class KetchupClient<State, Action> extends EventEmitter {
|
|
readonly ws: Websocket
|
|
synced?: KetchupSynced<State, Action>
|
|
clientId?: number
|
|
lastTimestamp?: number
|
|
|
|
constructor(
|
|
url: string,
|
|
readonly reducer: (state: State, action: Action) => State,
|
|
private readonly timeSource = Date.now
|
|
) {
|
|
super()
|
|
this.ws = new WebsocketBuilder(url)
|
|
.onClose(() => {
|
|
this.synced = undefined
|
|
this.clientId = undefined
|
|
this.lastTimestamp = undefined
|
|
})
|
|
.onOpen(ws => {
|
|
ws.send(JSON.stringify({
|
|
message: 'handshake',
|
|
timestamp: timeSource(),
|
|
} as HandshakeClientMessage))
|
|
})
|
|
.onMessage((_, event) => {
|
|
this.onMessage(JSON.parse(event.data))
|
|
})
|
|
.withBackoff(new ExponentialBackoff(100, 7))
|
|
.build()
|
|
}
|
|
|
|
private onMessage(msg: ServerMessage<State, Action>) {
|
|
switch (msg.message) {
|
|
case 'handshake':
|
|
const hMsg = msg as HandshakeServerMessage<State, Action>
|
|
this.clientId = hMsg.clientId
|
|
this.synced = new KetchupSynced(hMsg.confState, hMsg.confBefore, this.reducer, hMsg.unconfActions)
|
|
this.synced.on('projection', proj => this.emit('projection', proj))
|
|
this.emit('projection', this.synced.projState)
|
|
break
|
|
|
|
case 'action':
|
|
this.synced!.tryProcess(msg as ActionServerMessage<Action>)
|
|
break
|
|
|
|
case 'confirm':
|
|
this.synced!.confirmBefore((msg as ConfirmServerMessage).confBefore)
|
|
break
|
|
|
|
default:
|
|
throw new Error(`Unknown message from server: ${msg}`)
|
|
}
|
|
}
|
|
|
|
dispatch(action: Action, timestamp = this.timeSource()) {
|
|
// In case of timestamp collision, shift event 1ms into future until timestamp is novel.
|
|
// TODO: Better handling of events being submitted out of timestamp order
|
|
if (this.lastTimestamp !== undefined && timestamp <= this.lastTimestamp) {
|
|
timestamp = this.lastTimestamp + 1
|
|
}
|
|
|
|
if (this.synced?.tryProcess({ timestamp, action, clientId: this.clientId! })) {
|
|
this.lastTimestamp = timestamp
|
|
this.ws.send(JSON.stringify({
|
|
message: 'action',
|
|
action,
|
|
timestamp,
|
|
} as ActionClientMessage<Action>))
|
|
}
|
|
}
|
|
}
|