Skip to content

Commit

Permalink
Merge pull request #40 from nrkno/fix/empty-responses
Browse files Browse the repository at this point in the history
Various fixes around responses with no children
  • Loading branch information
nytamin authored Sep 20, 2024
2 parents 896e655 + de0be44 commit 24129fa
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 19 deletions.
152 changes: 144 additions & 8 deletions src/Ember/Client/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe('client', () => {
function createQualifiedNodeResponse(
path: string,
content: EmberElement,
children: Collection<NumberedTreeNode<EmberElement>>
children: Collection<NumberedTreeNode<EmberElement>> | undefined
): DecodeResult<Root> {
const parent = new QualifiedElementImpl<EmberElement>(path, content, children)

Expand All @@ -95,8 +95,10 @@ describe('client', () => {
fixLevel(child, node)
}
}
for (const child of Object.values<NumberedTreeNode<EmberElement>>(children)) {
fixLevel(child, parent as any as NumberedTreeNode<EmberElement>)
if (children) {
for (const child of Object.values<NumberedTreeNode<EmberElement>>(children)) {
fixLevel(child, parent as any as NumberedTreeNode<EmberElement>)
}
}
return {
value: {
Expand Down Expand Up @@ -169,11 +171,22 @@ describe('client', () => {

await new Promise(setImmediate)

// lookup on the parameter
expect(onSocketWrite).toHaveBeenCalledTimes(3)
socket.mockData({
value: {
1: new QualifiedElementImpl<EmberElement>(
'1.1.1',
new ParameterImpl(ParameterType.Boolean, 'On', undefined, false)
) as Exclude<RootElement, NumberedTreeNode<EmberElement>>,
},
})

await new Promise(setImmediate)

const res = await getByPathPromise
expect(res).toBeTruthy()
expect(res).toMatchObject(
new NumberedTreeNodeImpl(1, new ParameterImpl(ParameterType.Boolean, 'On', undefined, false))
)
expect(res?.contents).toMatchObject(new ParameterImpl(ParameterType.Boolean, 'On', undefined, false))
})
})

Expand Down Expand Up @@ -229,15 +242,34 @@ describe('client', () => {

await new Promise(setImmediate)

// Final lookup
// Final tree node
expect(onSocketWrite).toHaveBeenCalledTimes(6)
socket.mockData(
createQualifiedNodeResponse('1.1.1', new EmberNodeImpl('MAIN', undefined, undefined, false), {
1: new NumberedTreeNodeImpl(1, new ParameterImpl(ParameterType.Boolean, 'On', undefined, false)),
2: new NumberedTreeNodeImpl(1, new ParameterImpl(ParameterType.Boolean, 'Second', undefined, false)),
2: new NumberedTreeNodeImpl(2, new ParameterImpl(ParameterType.Boolean, 'Second', undefined, false)),
})
)

await new Promise(setImmediate)

// last call to the parameters just in case
expect(onSocketWrite).toHaveBeenCalledTimes(8)
socket.mockData(
createQualifiedNodeResponse(
'1.1.1.1',
new ParameterImpl(ParameterType.Boolean, 'On', undefined, false),
undefined
)
)
socket.mockData(
createQualifiedNodeResponse(
'1.1.1.2',
new ParameterImpl(ParameterType.Boolean, 'Second', undefined, false),
undefined
)
)

// Both completed successfully
const res = await getByPathPromise
expect(res).toBeTruthy()
Expand All @@ -246,4 +278,108 @@ describe('client', () => {
expect(res2).toBeTruthy()
})
})

it('getElementByPath empty node in the root', async () => {
await runWithConnection(async (client, socket) => {
// Do initial load
const getRootDirReq = await client.getDirectory(client.tree)
getRootDirReq.response?.catch(() => null) // Ensure uncaught response is ok
expect(onSocketWrite).toHaveBeenCalledTimes(1)
onSocketWrite.mockClear()

// Mock a valid response
socket.mockData({
value: {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl('Ruby', undefined, undefined, true)),
},
})
await getRootDirReq.response

// Request the empty node
const req = await client.getDirectory(client.tree[1])

// Returns empty node
expect(onSocketWrite).toHaveBeenCalledTimes(1)
socket.mockData({
value: {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl()),
},
})

await new Promise(setImmediate)

const res = await req.response
expect(res).toBeTruthy()
})
})

it('getElementByPath empty node in the tree', async () => {
await runWithConnection(async (client, socket) => {
// Do initial load
const getRootDirReq = await client.getDirectory(client.tree)
getRootDirReq.response?.catch(() => null) // Ensure uncaught response is ok
expect(onSocketWrite).toHaveBeenCalledTimes(1)
onSocketWrite.mockClear()

// Mock a valid response
socket.mockData({
value: {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl('Ruby', undefined, undefined, true)),
},
})
await getRootDirReq.response

// Run the tree
const getByPathPromise = client.getElementByPath('Ruby.Sums.Empty')

// First lookup
expect(onSocketWrite).toHaveBeenCalledTimes(1)
socket.mockData({
value: {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl('Ruby', undefined, undefined, true), {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl('Sums', undefined, undefined, true)),
}),
},
})

await new Promise(setImmediate)

// Second lookup
expect(onSocketWrite).toHaveBeenCalledTimes(2)
socket.mockData({
value: {
1: new QualifiedElementImpl<EmberElement>('1.1', new EmberNodeImpl('Sums', undefined, undefined, false), {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl('Empty', undefined, undefined, true)),
}) as Exclude<RootElement, NumberedTreeNode<EmberElement>>,
},
})

await new Promise(setImmediate)

const getByPathRes = await getByPathPromise
expect(getByPathRes).toBeTruthy()

const node = client.tree[1].children?.[1].children?.[1]
if (!node) throw new Error('Empty res') // really just a typeguard

// Request the empty node
const req = await client.getDirectory(node)

// lookup on the empty node
expect(onSocketWrite).toHaveBeenCalledTimes(3)
socket.mockData({
value: {
1: new QualifiedElementImpl<EmberElement>('1.1.1', new EmberNodeImpl()) as Exclude<
RootElement,
NumberedTreeNode<EmberElement>
>,
},
})

await new Promise(setImmediate)

const res = await req.response
expect(res).toBeTruthy()
})
})
})
42 changes: 36 additions & 6 deletions src/Ember/Client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { Connection, ConnectionDisposition, ConnectionOperation } from '../../mo
import { EmberNode } from '../../model/EmberNode'
import { EventEmitter } from 'eventemitter3'
import { S101Client } from '../Socket'
import { getPath, assertQualifiedEmberNode, insertCommand, updateProps } from '../Lib/util'
import { getPath, assertQualifiedEmberNode, insertCommand, updateProps, isEmptyNode } from '../Lib/util'
import { berEncode } from '../..'
import { NumberedTreeNodeImpl } from '../../model/Tree'
import { EmberFunction } from '../../model/EmberFunction'
Expand Down Expand Up @@ -70,6 +70,7 @@ export interface Subscription {
export interface Change {
path: string | undefined
node: RootElement
emptyNode?: boolean
}

export enum ConnectionStatus {
Expand Down Expand Up @@ -386,19 +387,27 @@ export class EmberClient extends EventEmitter<EmberClientEvents> {

while (pathArr.length) {
const i = pathArr.shift()
if (!i) break // TODO - this will break the loop if the path was `1..0`
if (i === undefined) break // TODO - this will break the loop if the path was `1..0`
if (!tree) break

let next = getNextChild(tree, i)
if (!next) {
const req = await this.getDirectory(tree)
tree = (await req.response) as NumberedTreeNode<EmberElement>
next = getNextChild(tree, i)
}
tree = next

if (!tree) throw new Error(`Could not find node ${i} on given path ${numberedPath.join()}`)
if (tree?.number !== undefined) numberedPath.push(tree.number)
}

if (tree?.contents.type === ElementType.Parameter) {
// do an additional getDirectory because Providers do not _have_ to send updates without that (should vs shall)
const req = await this.getDirectory(tree)
await req.response
}

if (cb && numberedPath) {
this._subscriptions.push({
path: numberedPath.join('.'),
Expand Down Expand Up @@ -505,7 +514,15 @@ export class EmberClient extends EventEmitter<EmberClientEvents> {
)
for (const req of reqs) {
// Don't complete the response, if the call was expecting the children to be loaded
if (req.nodeResponse === ExpectResponse.HasChildren && !change.node.children) continue
if (req.nodeResponse === ExpectResponse.HasChildren && !change.node.children) {
if (change.node.contents.type === ElementType.Parameter) {
// can't have children, therefore don't continue
} else if (change.emptyNode) {
// update comes from an empty node, so we can't continue anyway
} else {
continue
}
}

if (req.cb) req.cb(change.node)
if (req.resolve) {
Expand Down Expand Up @@ -588,9 +605,18 @@ export class EmberClient extends EventEmitter<EmberClientEvents> {
if (inserted) continue
changes.push(...this._updateTree(rootElement, tree))
} else {
if (this.tree[rootElement.number]) {
changes.push(...this._updateTree(rootElement, this.tree[rootElement.number]))
if (rootElement.children) {
if (this.tree[rootElement.number]) {
changes.push(...this._updateTree(rootElement, this.tree[rootElement.number]))
} else {
this.tree[rootElement.number] = rootElement
changes.push({ path: undefined, node: rootElement })
}
} else if (isEmptyNode(rootElement)) {
// empty node on the root of the tree must mean we have done a getDir on that specific node
changes.push({ path: rootElement.number + '', node: rootElement, emptyNode: true })
} else {
// this must have been something on the root of the tree (like GetDirectory)
this.tree[rootElement.number] = rootElement
changes.push({ path: undefined, node: rootElement })
}
Expand All @@ -605,7 +631,8 @@ export class EmberClient extends EventEmitter<EmberClientEvents> {
const changes: Array<Change> = []

if (update.contents.type === tree.contents.type) {
changes.push({ path: getPath(tree), node: tree })
changes.push({ path: getPath(tree), node: tree, emptyNode: isEmptyNode(update) })
// changes.push({ path: getPath(tree), node: tree })
switch (tree.contents.type) {
case ElementType.Node:
this._updateEmberNode(update.contents as EmberNode, tree.contents)
Expand All @@ -628,6 +655,9 @@ export class EmberClient extends EventEmitter<EmberClientEvents> {
} else if (update.children) {
changes.push({ path: getPath(tree), node: tree })
tree.children = update.children
for (const c of Object.values<NumberedTreeNode<EmberElement>>(update.children)) {
c.parent = tree
}
}

return changes
Expand Down
29 changes: 29 additions & 0 deletions src/Ember/Lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { QualifiedElement, NumberedTreeNode, RootElement } from '../../types/typ
import { EmberElement, ElementType } from '../../model/EmberElement'
import { Command } from '../../model/Command'
import { QualifiedElementImpl, NumberedTreeNodeImpl, TreeElement } from '../../model/Tree'
import { EmberNode } from '../../model'

export function assertQualifiedEmberNode(node: RootElement): Exclude<RootElement, NumberedTreeNode<EmberElement>> {
if ('path' in node) {
Expand Down Expand Up @@ -76,3 +77,31 @@ export function normalizeError(e: unknown): Error {

return new Error(typeof e === 'string' ? e : (e as any)?.toString())
}

export function isEmptyNode(node: TreeElement<EmberElement>): boolean {
const isNode = (node: TreeElement<EmberElement>): node is TreeElement<EmberNode> => {
return node.contents.type === ElementType.Node
}

if (!isNode(node)) {
return false
}

if (node.children) {
return false
}

if (
node.contents.description ??
node.contents.identifier ??
node.contents.isOnline ??
node.contents.isRoot ??
node.contents.schemaIdentifiers ??
node.contents.templateReference
) {
return false
}

// node is a node, node has no children, node has no properties set => node must be empty
return true
}
5 changes: 5 additions & 0 deletions src/Ember/Server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
MatrixImpl,
Matrix,
Connections,
EmberNodeImpl,
} from '../../model'
import {
Collection,
Expand Down Expand Up @@ -367,6 +368,10 @@ export class EmberServer extends EventEmitter<EmberServerEvents> {
qualified.children[i as unknown as number] = new NumberedTreeNodeImpl(child.number, child.contents)
}
}
} else if (qualified.contents.type === ElementType.Node && !('children' in tree && tree.children)) {
// node without children -> none of the properties should be set
qualified.contents = new EmberNodeImpl()
qualified.children = undefined
}
const data = berEncode([qualified as RootElement], RootType.Elements)
client.sendBER(data)
Expand Down
6 changes: 1 addition & 5 deletions src/encodings/ber/encoder/Tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,7 @@ function hasChildren(el: TreeElement<EmberElement>): boolean {
return (
'children' in el &&
el.children !== undefined &&
!(
el.contents.type === ElementType.Command ||
el.contents.type === ElementType.Parameter ||
el.contents.type === ElementType.Template
)
!(el.contents.type === ElementType.Command || el.contents.type === ElementType.Template)
)
}

Expand Down

0 comments on commit 24129fa

Please sign in to comment.