Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple <mos> and </mos> tags in messages #85

Merged
merged 2 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/connector/src/MosConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ export class MosConnection extends EventEmitter implements IMosConnection {
} else {
// Unknown / internal error
// Log error:
this.emit('warning', 'Error when handling incoming data: ' + err)
this.emit('warning', `Error when handling incoming data: ${err} ${err?.stack}`)
// reply with NACK:
// TODO: implement ACK
// https://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOS_Protocol_Version_2.8.5_Final.htm#mosAck
Expand Down
85 changes: 85 additions & 0 deletions packages/connector/src/__tests__/MessageChunking.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
getMosConnection,
getMosDevice,
getXMLReply,
makeFakeIncomingMessage,
mosTypes,
sendFakeIncomingMessage,
setupMocks,
} from './lib'
import {
Expand Down Expand Up @@ -102,6 +104,9 @@ describe('message chunking', () => {
serverSocketMockUpper = b.serverSocketMockUpper
serverSocketMockQuery = b.serverSocketMockQuery
})
afterAll(async () => {
await mosConnection.dispose()
})
beforeEach(() => {
onRunningOrderStory.mockClear()

Expand Down Expand Up @@ -226,4 +231,84 @@ describe('message chunking', () => {
expect(returnedObj).toMatchObject(xmlApiData.roList2)
expect(returnedObj).toMatchSnapshot()
})

test('incoming chunked message', async () => {
let onRequestMOSObject: jest.Mock<any, any>
onRequestMOSObject = jest.fn(async () => {
return xmlApiData.mosObj
})
onRequestMOSObject.mockClear()
mosDevice.onRequestMOSObject(async (objId: string): Promise<IMOSObject | null> => {
return onRequestMOSObject(objId)
})

const message = makeFakeIncomingMessage(`<mosReqObj><objID>M000123</objID></mosReqObj>`)

const chunks = [message.message.slice(0, 100), message.message.slice(100)]

// Send first part of the message:
await sendFakeIncomingMessage(serverSocketMockLower, chunks[0])
expect(onRequestMOSObject).toHaveBeenCalledTimes(0)

// Send rest of the message:
await sendFakeIncomingMessage(serverSocketMockLower, chunks[1])
expect(onRequestMOSObject).toHaveBeenCalledTimes(1)
})

test('multiple mos tags', async () => {
let onRequestMOSObject: jest.Mock<any, any>
onRequestMOSObject = jest.fn(async () => {
return xmlApiData.mosObj
})
onRequestMOSObject.mockClear()
mosDevice.onRequestMOSObject(async (objId: string): Promise<IMOSObject | null> => {
return onRequestMOSObject(objId)
})

const message = makeFakeIncomingMessage(
`<mosReqObj><objID>M000123</objID><mos><test>hehehe</test></mos></mosReqObj>`
)

const i0 = message.message.indexOf('<mos>', 10)
const i1 = message.message.indexOf('</mos>', i0 + 1)
const i2 = message.message.indexOf('</mos>', i1 + 1)

const chunks = [
message.message.slice(0, i0),
message.message.slice(i0, i1),
message.message.slice(i1, i2),
message.message.slice(i2),
]

// Send the parts of the message:
await sendFakeIncomingMessage(serverSocketMockLower, chunks[0])
expect(onRequestMOSObject).toHaveBeenCalledTimes(0)
await sendFakeIncomingMessage(serverSocketMockLower, chunks[1])
expect(onRequestMOSObject).toHaveBeenCalledTimes(0)
await sendFakeIncomingMessage(serverSocketMockLower, chunks[2])
expect(onRequestMOSObject).toHaveBeenCalledTimes(0)
await sendFakeIncomingMessage(serverSocketMockLower, chunks[2])
expect(onRequestMOSObject).toHaveBeenCalledTimes(1)
})
test('multiple messages', async () => {
let onRequestMOSObject: jest.Mock<any, any>
onRequestMOSObject = jest.fn(async () => {
return xmlApiData.mosObj
})
onRequestMOSObject.mockClear()
mosDevice.onRequestMOSObject(async (objId: string): Promise<IMOSObject | null> => {
return onRequestMOSObject(objId)
})

const message0 = makeFakeIncomingMessage(
`<mosReqObj><objID>M000123</objID><mos><test>hehehe</test></mos></mosReqObj>`
)
const message1 = makeFakeIncomingMessage(
`<mosReqObj><objID>M000124</objID><mos><test>hahaha</test></mos></mosReqObj>`
)

// Send both messages right away:
await sendFakeIncomingMessage(serverSocketMockLower, message0.message + message1.message)
expect(onRequestMOSObject).toHaveBeenCalledTimes(2)
})
})
21 changes: 17 additions & 4 deletions packages/connector/src/__tests__/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,24 @@ export async function fakeIncomingMessage(
ourMosId?: string,
theirMosId?: string
): Promise<number> {
fakeIncomingMessageMessageId++
const fullMessage = getXMLReply(fakeIncomingMessageMessageId, message, ourMosId, theirMosId)
socketMockLower.mockReceiveMessage(encode(fullMessage))
const m = makeFakeIncomingMessage(message, ourMosId, theirMosId)
sendFakeIncomingMessage(socketMockLower, m.message)

return Promise.resolve(fakeIncomingMessageMessageId)
return Promise.resolve(m.messageId)
}
export function makeFakeIncomingMessage(
message: string,
ourMosId?: string,
theirMosId?: string
): { messageId: number; message: string } {
fakeIncomingMessageMessageId++
return {
message: getXMLReply(fakeIncomingMessageMessageId, message, ourMosId, theirMosId),
messageId: fakeIncomingMessageMessageId,
}
}
export function sendFakeIncomingMessage(socketMockLower: SocketMock, message: string): void {
socketMockLower.mockReceiveMessage(encode(message))
}
export function getXMLReply(
messageId: string | number,
Expand Down
82 changes: 69 additions & 13 deletions packages/connector/src/connection/mosMessageParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,30 +36,70 @@ export class MosMessageParser extends EventEmitter {

let messageString: string | undefined

const startIndex = this.dataChunks.indexOf(startMatch)
if (startIndex === -1) {
const startIndexes = this.indexesOf(this.dataChunks, startMatch)
if (startIndexes.length === 0) {
// No start tag, so looks like we have jibberish
this.dataChunks = ''
} else {
if (startIndex > 0) {
const junkStr = this.dataChunks.slice(0, startIndex)
const firstStartIndex = startIndexes[0]
if (firstStartIndex > 0) {
const junkStr = this.dataChunks.slice(0, firstStartIndex)
this.debugTrace(`${this.description} Discarding message fragment: "${junkStr}"`)

// trim off anything before <mos>, as we'll never be able to parse that anyway.
this.dataChunks = this.dataChunks.slice(startIndex)
this.dataChunks = this.dataChunks.slice(firstStartIndex)
}

const endIndex = this.dataChunks.indexOf(endMatch)
if (endIndex >= 0) {
// We have an end too, so pull out the message
const endIndex2 = endIndex + endMatch.length
messageString = this.dataChunks.slice(0, endIndex2)
this.dataChunks = this.dataChunks.slice(endIndex2)
const endIndexes = this.indexesOf(this.dataChunks, endMatch)
if (endIndexes.length > 0) {
// We have an end tag too

// parse our xml
/** null = message is not complete */
let useEndIndex: number | null = null

if (startIndexes.length === 1 && endIndexes.length === 1) {
// fast-path:
useEndIndex = endIndexes[0]
} else {
const tags: { start: boolean; index: number }[] = [
...startIndexes.map((index) => ({
start: true,
index,
})),
...endIndexes.map((index) => ({
start: false,
index,
})),
].sort((a, b) => a.index - b.index)

// Figure out where in the message the end tag closes the start tag:
let tagBalance = 0
for (const tag of tags) {
if (tag.start) tagBalance++
else tagBalance--

if (tagBalance < 0) {
// Hmm, something is wrong, there should never be more end tags than start tags

// trim off anything before this end tag, we'll never be able to parse that anyway.
this.dataChunks = this.dataChunks.slice(tag.index + endMatch.length)
break
} else if (tagBalance === 0) {
// We have a complete message, so pluck it out
useEndIndex = tag.index

break
}
}
}

if (useEndIndex !== null) {
const endIndex2 = useEndIndex + endMatch.length
messageString = this.dataChunks.slice(0, endIndex2)
this.dataChunks = this.dataChunks.slice(endIndex2)
}
}
}

let parsedData: any | null = null
try {
if (messageString) {
Expand Down Expand Up @@ -87,4 +127,20 @@ export class MosMessageParser extends EventEmitter {
}
}
}
/** Returns a list of indexes for the occurences of searchString in str */
private indexesOf(str: string, searchString: string): number[] {
if (!searchString.length) throw new Error('searchString cannot be empty')
const indexes: number[] = []

let prevIndex = 0
for (let i = 0; i < str.length; i++) {
// ^ Just to avoid an infinite loop

const index = str.indexOf(searchString, prevIndex)
if (index === -1) break
indexes.push(index)
prevIndex = index + searchString.length
}
return indexes
}
}