Skip to content
This repository has been archived by the owner on Nov 10, 2022. It is now read-only.

Commit

Permalink
feat: implement update-state event
Browse files Browse the repository at this point in the history
Note that the state is not propagated to the other clients, after the
state had been succesfully updated.
In effect, the only purpose of the state is to give a new user its
initial state.

Its the duty of every client to send their changes directly to the other
clients (that's the idea of websockets)
  • Loading branch information
severo committed Jan 22, 2020
1 parent c1af81c commit 555a8be
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 0 deletions.
23 changes: 23 additions & 0 deletions src/domain/events/toclient/state.event.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { expect } from 'chai'
import { StateEvent } from './state.event'

describe('Events', () => {
describe('StateEvent', () => {
it("should have 'state' event as name", () => {
expect(StateEvent.eventName).to.equal('state')
})
it('should initialize an object', () => {
// arrange
const state: object = {
points: [{ x: 1, y: 2 }],
imageSrc: 'lake.png',
}

// act
const event = new StateEvent(state)

// assert
expect(event.state).to.deep.equal(state)
})
})
})
6 changes: 6 additions & 0 deletions src/domain/events/toclient/state.event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { EventToClient } from './event.to.client'

export class StateEvent {
static eventName: string = EventToClient.State
constructor(public readonly state: object) {}
}
1 change: 1 addition & 0 deletions src/domain/events/toserver/event.to.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export class EventToServer {
// static readonly KickRoomGuest = 'kick-room-guest'
// static readonly ListRooms = 'list-rooms'

static readonly UpdateState = 'update-state'
static readonly UpdateUserName = 'update-user-name'
static readonly UpdateUserColor = 'update-user-color'
// static readonly ListUsers = 'list-users'
Expand Down
1 change: 1 addition & 0 deletions src/domain/events/toserver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './connection.event'
// export * from "./kick.room.guest.event";
// export * from "./list.rooms.event";

export * from './update.state.event'
export * from './update.user.name.event'
export * from './update.user.color.event'
// export * from "./list.users.event";
73 changes: 73 additions & 0 deletions src/domain/events/toserver/update.state.event.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect } from 'chai'
import { UpdateStateEvent } from './update.state.event'
import Automerge from 'automerge'

describe('Events', () => {
describe('UpdateStateEvent', () => {
it("should have 'update-state' event as name", () => {
expect(UpdateStateEvent.eventName).to.equal('update-state')
})
it('should initialize an array of Change objects', () => {
// arrange
const changes: Automerge.Change[] = [
{
ops: [
{
action: 'makeList',
obj: 'be89e397-324c-4bec-932a-ae087a3177de',
},
{
action: 'ins',
obj: 'be89e397-324c-4bec-932a-ae087a3177de',
key: '_head',
elem: 1,
},
{
action: 'makeMap',
obj: '8b94a2c5-98c3-4ed9-8a18-ccae369cf168',
},
{
action: 'set',
obj: '8b94a2c5-98c3-4ed9-8a18-ccae369cf168',
key: 'x',
value: 1,
},
{
action: 'set',
obj: '8b94a2c5-98c3-4ed9-8a18-ccae369cf168',
key: 'y',
value: 2,
},
{
action: 'link',
obj: 'be89e397-324c-4bec-932a-ae087a3177de',
key: 'b6336846-12b7-4d8c-a4ab-949a3afd1903:1',
value: '8b94a2c5-98c3-4ed9-8a18-ccae369cf168',
},
{
action: 'link',
obj: '00000000-0000-0000-0000-000000000000',
key: 'points',
value: 'be89e397-324c-4bec-932a-ae087a3177de',
},
{
action: 'set',
obj: '00000000-0000-0000-0000-000000000000',
key: 'imageSrc',
value: 'lake.png',
},
],
actor: 'b6336846-12b7-4d8c-a4ab-949a3afd1903',
seq: 1,
deps: {},
},
]

// act
const event = new UpdateStateEvent(changes)

// assert
expect(event.data).to.deep.equal(changes)
})
})
})
7 changes: 7 additions & 0 deletions src/domain/events/toserver/update.state.event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { EventToServer } from './event.to.server'
import Automerge from 'automerge'

export class UpdateStateEvent {
static eventName: string = EventToServer.UpdateState
constructor(public readonly data: Automerge.Change[]) {}
}
155 changes: 155 additions & 0 deletions src/socket.io/socket.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ chai.use(chaiThings)
import { MockLogger } from '../shared/index'
import { Socket } from './socket'
import {
UpdateStateEvent,
UpdateUserNameEvent,
UpdateUserColorEvent,
} from '../domain/events/toserver'
Expand Down Expand Up @@ -92,6 +93,7 @@ describe('Server', () => {
// after
newClient.disconnect()
})

it('should send an empty state to a new user meanwhile the state has not been changed', async () => {
// arrange
const getStateEvent = (): Promise<object> =>
Expand All @@ -107,6 +109,159 @@ describe('Server', () => {
})
})

describe('update-state', () => {
let updateState: (
args: UpdateStateEventArgs
) => Promise<UpdateStateAckArgs>

beforeEach(async () => {
await new Promise(resolve => client.on('connect', resolve))
updateState = args =>
new Promise(resolve =>
client.emit(
UpdateStateEvent.eventName,
new UpdateStateEvent(args).data,
resolve
)
)
})

it('should log info message for empty data, and updated should be false', async () => {
// arrange
const args: undefined = undefined

// act
const value = await updateState(args)

// assert
expect(value).to.not.be.undefined
expect(value).to.have.property('updated', false)
expect(value).to.have.property('error')
expect(value.error).to.have.property('name', 'TypeError')
expect(value.error).to.have.property(
'message',
'object null is not iterable (cannot read property Symbol(Symbol.iterator))'
)
mockLogger
.getInfoLogs()
.should.include.an.item.that.equals(
`State could not be updated - object null is not iterable (cannot read property Symbol(Symbol.iterator))`
)
})

it('should log success info message for empty changes array, and updated should be true, and error should not exist', async () => {
// arrange
const args: UpdateStateEventArgs = []

// act
const value = await updateState(args)

// assert
expect(value).to.not.be.undefined
expect(value).to.have.property('updated', true)
expect(value).to.not.have.property('error')
mockLogger
.getInfoLogs()
.should.include.something.that.equals(`State updated`)
})

// arrange
const newState = {
points: [{ x: 1, y: 2 }],
imageSrc: 'lake.png',
}
const changes: UpdateStateEventArgs = [
{
ops: [
{
action: 'makeList',
obj: 'be89e397-324c-4bec-932a-ae087a3177de',
},
{
action: 'ins',
obj: 'be89e397-324c-4bec-932a-ae087a3177de',
key: '_head',
elem: 1,
},
{
action: 'makeMap',
obj: '8b94a2c5-98c3-4ed9-8a18-ccae369cf168',
},
{
action: 'set',
obj: '8b94a2c5-98c3-4ed9-8a18-ccae369cf168',
key: 'x',
value: 1,
},
{
action: 'set',
obj: '8b94a2c5-98c3-4ed9-8a18-ccae369cf168',
key: 'y',
value: 2,
},
{
action: 'link',
obj: 'be89e397-324c-4bec-932a-ae087a3177de',
key: 'b6336846-12b7-4d8c-a4ab-949a3afd1903:1',
value: '8b94a2c5-98c3-4ed9-8a18-ccae369cf168',
},
{
action: 'link',
obj: '00000000-0000-0000-0000-000000000000',
key: 'points',
value: 'be89e397-324c-4bec-932a-ae087a3177de',
},
{
action: 'set',
obj: '00000000-0000-0000-0000-000000000000',
key: 'imageSrc',
value: 'lake.png',
},
],
actor: 'b6336846-12b7-4d8c-a4ab-949a3afd1903',
seq: 1,
deps: {},
},
]

it('should log info message for non-empty valid changes array, updated should be true, and error should not exist', async () => {
// act
const value = await updateState(changes)

// assert
expect(value).to.not.be.undefined
expect(value).to.have.property('updated', true)
expect(value).to.not.have.property('error')
mockLogger
.getInfoLogs()
.should.include.something.that.equals(`State updated`)
})

it('should send the current state to a new client, after the state had been updated', async () => {
// arrange
let newClient: SocketIOClient.Socket
const getStateEvent = (): Promise<object> => {
return new Promise(resolve => {
newClient = ioClient.connect(
socketUrl + '/occupapp-beta',
options
)
newClient.on(StateEvent.eventName, resolve)
})
}

// act
await updateState(changes)
const state = await getStateEvent()

// assert
expect(state).to.deep.equal(newState)

// after
newClient.disconnect()
})
})

describe('update-user-name', () => {
let updateNameUser: (
args: UpdateUserNameEventArgs
Expand Down
32 changes: 32 additions & 0 deletions src/socket.io/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Constants } from './constants'
import { Guard, ConsoleLogger } from '../shared/index'
import {
ConnectionEvent,
UpdateStateEvent,
UpdateUserNameEvent,
UpdateUserColorEvent,
} from '../domain/events/toserver'
Expand Down Expand Up @@ -96,6 +97,37 @@ class Socket {
ack({ updated: true })
}

socket.on(
UpdateStateEvent.eventName,
(data: UpdateStateEventArgs, ack: UpdateStateAck) => {
try {
updateState(data, ack)
} catch (error) {
this.log.info('State could not be updated', error.message)
ack({
updated: false,
error: this.toException(error),
})
}
}
)

const updateState = (
data: UpdateStateEventArgs,
ack: UpdateStateAck
) => {
// We let automerge throw if the data is malformed
this.state = Automerge.applyChanges(this.state, data)

// The server doesn't send the changes to the other clients
// It's the task of the client that updated the state to send it to
// them. Using websockets, the latency between two clients will be
// lower than adding an intermediate step through the server)

this.log.info('State updated')
ack({ updated: true })
}

// const roomJoin = (data: RoomJoinEventArgs, ack: RoomJoinAck) => {
// Guard.throwIfObjectUndefined(data, Constants.dataIsRequired)
// Guard.throwIfStringNotDefinedOrEmpty(
Expand Down
12 changes: 12 additions & 0 deletions src/types/events/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ declare interface Exception {
message: string
}

// See https://stackoverflow.com/a/51114250/7351594
type UpdateStateEventArgs = import('automerge').Change[]

declare interface UpdateStateAck {
($data: UpdateStateAckArgs): void
}

declare interface UpdateStateAckArgs {
updated: boolean
error?: Exception
}

declare interface UpdateUserNameEventArgs {
name: string
}
Expand Down

0 comments on commit 555a8be

Please sign in to comment.