Skip to content
36 changes: 24 additions & 12 deletions docs/data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -129,18 +129,6 @@ To set any numeric type, including enumerations:
output.instance.setNumber('my_double', 2.14)
output.instance.setNumber('my_enum', 2)

.. warning::
The range of values for a numeric field is determined by the type
used to define that field in the configuration file. However,
``setNumber`` and ``getNumber`` can't handle 64-bit integers
(*int64* and *uint64*) whose absolute values are larger than 2^53.
This is a *Connector* limitation due to the use of *double* as an
intermediate representation.

When ``setNumber`` or ``getNumber`` detect this situation, they will raise
an :class:`DDSError`. ``getJson`` and ``setJson`` do not have this
limitation and can handle any 64-bit integer.

To set booleans:

.. code-block::
Expand Down Expand Up @@ -202,6 +190,30 @@ getter: :meth:`SampleIterator.getNumber()`, :meth:`SampleIterator.getBoolean()`,
a value that can be interpreted as a number, ``sample.get('my_string')`` returns
a number, not a string.

Accessing 64-bit integers
^^^^^^^^^^^^^^^^^^^^^^^^^
Internally, *Connector* relies on a framework that only contains a single number
type, which is an IEEE-754 floating-point number. Additionally, *Connector* does not use
JavaScript's BigInt representation for numbers, meaning JavaScript has this same limitation.
As a result, not all 64-bit integers can be represented with exact precision.
If your type contains uint64 or int64 members, and you expect them to be larger
Comment thread
samuelraeburn marked this conversation as resolved.
Outdated
than ``2^53``, then you must take the following into account.

64-bit values larger than 2^53 can be set via:
- The type-agnostic setter (``instance.set()``), if they are supplied as strings, e.g., ``the_output.instance.set('my_uint64', '18446744073709551615')``.
- The ``setString`` setter, e.g., ``the_output.instance.setString('my_uint64', '18446744073709551615')``.
- Via a ``setFromJson``, if they are provided as strings, e.g., ``the_output.instance.setFromJson({my_uint64: '18446744073709551615'})``.

64-bit values larger than 2^53 can be retrieved via:
- The type-agnostic getter, ``getValue``. If the value is smaller than 2^53, it will be returned as a number; otherwise it will be returned as a string:
e.g., ``sample.get(0).get('my_int64') // e.g., '9223372036854775807' OR 1234``.
- The ``getString`` method.
The value will be returned as a string, e.g., ``sample.getString(my_int64') // '9223372036854775807'``.

.. warning::

If getNumber or setNumber is used with a value larger than 2^53, that value will produce an ``Error``.

Accessing structs
^^^^^^^^^^^^^^^^^

Expand Down
16 changes: 10 additions & 6 deletions rticonnextdds-connector.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,13 +321,17 @@ function _getAnyValue (getter, connector, inputName, index, fieldName) {
return !!boolVal.deref()
} else if (selection === _AnyValueKind.connector_string) {
const nodeStr = _moveCString(stringVal.deref())
// Try to convert the returned string to a JSON object. We can now return
// one of two things:
// If this is NOT a numeric string, try to convert the returned string to a
// JSON object. We can now return one of two things:
// - An actual string (if the JSON.parse call fails)
// - A JSON object (if the JSON.parse call succeeds)
try {
return JSON.parse(nodeStr)
} catch (err) {
if (isNaN(nodeStr)) {
try {
return JSON.parse(nodeStr)
} catch (err) {
return nodeStr
}
} else {
return nodeStr
}
} else {
Expand Down Expand Up @@ -1331,7 +1335,7 @@ class Instance {
* Sets a string field.
*
* @param {string} fieldName - The name of the field.
* @param {number} value - A string value, or null, to unset an
* @param {string|null} value - A string value, or null, to unset an
* optional member.
*/
setString (fieldName, value) {
Expand Down
21 changes: 11 additions & 10 deletions test/nodejs/test_rticonnextdds_connector.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,45 +42,46 @@ describe('Connector Tests', function () {
})

it('Connector should get instantiated for valid' +
'xml and participant profile', function () {
'xml and participant profile', async function () {
const participantProfile = 'MyParticipantLibrary::Zero'
const xmlProfile = path.join(__dirname, '/../xml/TestConnector.xml')
const connector = new rti.Connector(participantProfile, xmlProfile)
expect(connector).to.exist
expect(connector).to.be.instanceOf(rti.Connector)
connector.close()
await connector.close()
})

it('Multiple Connector objects can be instantiated', () => {
it('Multiple Connector objects can be instantiated', async () => {
const participantProfile = 'MyParticipantLibrary::Zero'
const xmlProfile = path.join(__dirname, '/../xml/TestConnector.xml')
const connectors = []
for (let i = 0; i < 3; i++) {
connectors.push(new rti.Connector(participantProfile, xmlProfile))
}
connectors.forEach((connector) => {

connectors.forEach(async (connector) => {
expect(connector).to.exist
expect(connector).to.be.instanceOf(rti.Connector)
connector.close()
await connector.close()
})
})

// Test for CON-163
it('Multiple Connector obejcts can be instantiated without participant QoS', () => {
it('Multiple Connector obejcts can be instantiated without participant QoS', async () => {
const participantProfile = 'MyParticipantLibrary::MyParticipant'
const xmlProfile = path.join(__dirname, '/../xml/TestConnector3.xml')
const connectors = []
for (let i = 0; i < 2; i++) {
connectors.push(new rti.Connector(participantProfile, xmlProfile))
}
connectors.forEach((connector) => {
connectors.forEach(async (connector) => {
expect(connector).to.exist
expect(connector).to.be.instanceOf(rti.Connector)
connector.close()
await connector.close()
})
})

it('Load two XML files using the url group syntax', function () {
it('Load two XML files using the url group syntax', async function () {
const xmlProfile1 = path.join(__dirname, '/../xml/TestConnector.xml')
const xmlProfile2 = path.join(__dirname, '/../xml/TestConnector2.xml')
const fullXmlPath = xmlProfile1 + ';' + xmlProfile2
Expand All @@ -89,7 +90,7 @@ describe('Connector Tests', function () {
expect(connector).to.be.instanceOf(rti.Connector)
const output = connector.getOutput('MyPublisher2::MySquareWriter2')
expect(output).to.exist
connector.close()
await connector.close()
})

it('Should be possible to create a Connector with participant qos', function () {
Expand Down
93 changes: 83 additions & 10 deletions test/nodejs/test_rticonnextdds_data_access.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const os = require('os')
const ffi = require('ffi-napi')
const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
const { deepStrictEqual } = require('assert')
const expect = chai.expect
chai.config.includeStack = true
chai.use(chaiAsPromised)
Expand Down Expand Up @@ -90,10 +91,10 @@ describe('Data access tests with a pre-populated input', function () {
expect(sample.validData).to.be.true
})

afterEach(() => {
afterEach(async () => {
// Take all samples here to ensure that next test case has a clean input
prepopulatedInput.take()
connector.close()
await connector.close()
})

it('getNumber should return a number', () => {
Expand All @@ -115,12 +116,13 @@ describe('Data access tests with a pre-populated input', function () {

it('getString on a number field should return a string', () => {
expect(sample.getString('my_long')).to.deep.equals('10').and.is.a('string')
expect(sample.getString('my_double')).to.deep.equals('3.3').and.is.a('string')
// Even though 3.3 was set, it cannot be perfectly represetned as a double
expect(sample.getString('my_double')).to.deep.equals('3.2999999999999998').and.is.a('string')
})

it('getString requires a valid index', () => {
expect(() => {
prepopulatedInput.samples.getString('NAN', 'my_string')
prepopulatedInput.samples.getString('NaN', 'my_string')
}).to.throw(TypeError)
})

Expand Down Expand Up @@ -458,10 +460,10 @@ describe('Tests with a testOutput and testInput', () => {
}
})

afterEach(() => {
afterEach(async () => {
// Take all samples here to ensure that next test case has a clean input
testInput.take()
connector.close()
await connector.close()
})

if (os.platform() !== 'win32') {
Expand Down Expand Up @@ -645,8 +647,7 @@ describe('Tests with a testOutput and testInput', () => {
}
testInput.take()
const theNumericString = testInput.samples.get(0).get('my_string')
// Due to CON-139 get returns strings as numbers if they represent a number
expect(theNumericString).to.be.a('number').and.deep.equals(1234)
expect(theNumericString).to.be.a('string').and.deep.equals('1234')
})

it('Test output sequences', async () => {
Expand Down Expand Up @@ -1148,6 +1149,78 @@ describe('Tests with a testOutput and testInput', () => {
testInput.take()
expect(testInput.samples.get(0).get('my_enum')).to.deep.equals(1)
})

// Both Lua v5.2 (used within Connector native libraries) and JavaScript have
// the same restriction on 64-bit integers - their only Number type is a double
// precision floating point value, meaning they cannot accurately represent
// integers large than 2^53.
// These tests check that via the type-agnostic getter and setter, and getString
// and setString can be used to workaround this limitation.
describe('Tests with 64-bit integers', () => {
it('Can communicate large 64-bit numbers using getString and setString', async () => {
testOutput.instance.setString('my_uint64', '18446744073709551615')
testOutput.instance.setString('my_int64', '9223372036854775807')
testOutput.write()
try {
await testInput.wait(testExpectSuccessTimeout)
} catch (err) {
console.log('Error caught: ' + err)
expect(false).to.deep.equals(true)
}
testInput.take()
expect(testInput.samples.get(0).getString('my_uint64')).to.deep.equals('18446744073709551615')
expect(testInput.samples.get(0).getString('my_int64')).to.deep.equals('9223372036854775807')
})

it('64-bit values larger than 2^53 are returned as strings by get', async () => {
const maxInt64 = '9223372036854775807'
const maxUint64 = '18446744073709551615'
testOutput.instance.setFromJson({ my_int64: maxInt64, my_uint64: maxUint64 })
testOutput.write()
try {
await testInput.wait(testExpectSuccessTimeout)
} catch (err) {
console.log('Error caught: ' + err)
expect(false).to.deep.equals(true)
}
testInput.take()
expect(testInput.samples.get(0).get('my_uint64')).to.be.a.string
expect(testInput.samples.get(0).get('my_uint64')).to.deep.equals(maxUint64)
expect(testInput.samples.get(0).get('my_int64')).to.deep.equals(maxInt64)
expect(testInput.samples.get(0).get('my_int64')).to.be.a.string
})

it('64-bit values smaller or equal to 2^53 are returned as numbers by get', async () => {
testOutput.instance.setFromJson({ my_int64: 123456, my_uint64: 123456 })
testOutput.write()
try {
await testInput.wait(testExpectSuccessTimeout)
} catch (err) {
console.log('Error caught: ' + err)
expect(false).to.deep.equals(true)
}
testInput.take()
expect(testInput.samples.get(0).get('my_uint64')).to.deep.equals(123456)
expect(testInput.samples.get(0).get('my_uint64')).to.be.a('number')
expect(testInput.samples.get(0).get('my_int64')).to.deep.equals(123456)
expect(testInput.samples.get(0).get('my_int64')).to.be.a('number')
})

it('Can communicate large 64-bit numbers using type-agnostic getters and setters', async () => {
testOutput.instance.set('my_uint64', '18446744073709551615')
testOutput.instance.set('my_int64', '9223372036854775807')
testOutput.write()
try {
await testInput.wait(testExpectSuccessTimeout)
} catch (err) {
console.log('Error caught: ' + err)
expect(false).to.deep.equals(true)
}
testInput.take()
expect(testInput.samples.get(0).get('my_uint64')).to.deep.equals('18446744073709551615')
expect(testInput.samples.get(0).get('my_int64')).to.deep.equals('9223372036854775807')
})
})
})

describe('Tests with two readers and two writers', () => {
Expand Down Expand Up @@ -1188,11 +1261,11 @@ describe('Tests with two readers and two writers', () => {
}
})

afterEach(() => {
afterEach(async () => {
// Take any data
testInput1.take()
testInput2.take()
connector.close()
await connector.close()
})

// Since we have not written any data, all different forms of wait for data
Expand Down
8 changes: 4 additions & 4 deletions test/nodejs/test_rticonnextdds_data_iterators.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ describe('Test the iteration of Input Samples', () => {
expect(input.samples.length).to.deep.equals(expectedSampleCount)
})

afterEach(() => {
connector.close()
afterEach(async () => {
await connector.close()
})

it('Check sample iterator and iterable', () => {
Expand Down Expand Up @@ -254,7 +254,7 @@ describe('Test dispose', () => {
})

afterEach(async () => {
connector.close()
await connector.close()
})

it('Dispose should not have validData set to true', () => {
Expand Down Expand Up @@ -325,7 +325,7 @@ describe('Test unregister', () => {
})

afterEach(async () => {
connector.close()
await connector.close()
})

it('Unregister should not have validData set to true', () => {
Expand Down
4 changes: 2 additions & 2 deletions test/nodejs/test_rticonnextdds_dataflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ params.forEach((retrievalMethod) => {
})

// Clean-up after all tests execute
after(function () {
after(async function () {
this.timeout(0)
connector.close()
await connector.close()
})

// Initialization done before each test executes
Expand Down
14 changes: 7 additions & 7 deletions test/nodejs/test_rticonnextdds_discovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,21 +85,21 @@ const getDiscoveryWriterOnlyOutput = () => {
return output
}

const cleanupConnectors = () => {
const cleanupConnectors = async () => {
if (discoveryConnector !== null) {
discoveryConnector.close()
await discoveryConnector.close()
discoveryConnector = null
}
if (discoveryConnectorNoEntityNames !== null) {
discoveryConnectorNoEntityNames.close()
await discoveryConnectorNoEntityNames.close()
discoveryConnectorNoEntityNames = null
}
if (readerOnlyConnector !== null) {
readerOnlyConnector.close()
await readerOnlyConnector.close()
readerOnlyConnector = null
}
if (writerOnlyConnector !== null) {
writerOnlyConnector.close()
await writerOnlyConnector.close()
writerOnlyConnector = null
}
}
Expand Down Expand Up @@ -285,7 +285,7 @@ describe('Discovery tests', function () {
expect(matches).to.deep.include.members([{ name: 'TestWriter' }])

// Delete the Connector object that the input is within
readerOnlyConnector.close()
await readerOnlyConnector.close()
readerOnlyConnector = null

// The output should unmatch from the input
Expand Down Expand Up @@ -343,7 +343,7 @@ describe('Discovery tests', function () {
expect(matches).to.deep.include.members([{ name: 'TestReader' }])

// Delete the Connector object that the output is within
writerOnlyConnector.close()
await writerOnlyConnector.close()
writerOnlyConnector = null

// The input should unmatch from the output
Expand Down
4 changes: 2 additions & 2 deletions test/nodejs/test_rticonnextdds_input.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ describe('Subscriber not automatically enabled tests', () => {
expect(connector).to.exist.and.to.be.instanceOf(rti.Connector)
})

after(() => {
connector.close()
after(async () => {
await connector.close()
})

it('Entities should not auto-discover each other if QoS is set appropriately', async () => {
Expand Down
Loading