Learning
Community
Open Exchange
Global Masters
InterSystems IRIS Data Platform 2019.3 / Application Development / Using the Native API for Node.js / Working with Global Arrays
Previous section   Next section

Working with Global Arrays

This chapter covers the following topics:
Note:
The examples in this chapter assume that an Iris object named native already exists and is connected to the server. The following code was used to create it:
  const IRISNative = require('intersystems-iris-native')
  let connectionInfo = {host:'127.0.0.1', port:51773, ns:'USER', user:'_SYSTEM', pwd:'SYS'};
  const conn = IRISNative.createConnection(connectionInfo);
  const native = conn.createIris();
For more information, see the Quick Reference entries for createConnection() and createIris().

Creating, Updating, and Deleting Nodes

This section describes the Native API methods used to create, update, and delete nodes. set(), increment(), and kill() are the only methods that can create a global array or alter its contents. The following examples demonstrate how to use each of these methods.
Setting and changing node values
Iris.set() takes a value argument and stores the value at the specified address.
If no node exists at that address, a new one is created.
The set() method can assign values of any supported datatype. In the following example, the first call to set() creates a new node at subnode address myGlobal('A') and sets the value of the node to string 'first'. The second call changes the value of the subnode, replacing it with integer 1.
  native.set('first', 'myGlobal', 'A');     // create node myGlobal('A') = 'first'
  native.set(1, 'myGlobal', 'A');   // change value of myGlobal('A') to 1.
set() is a polymorphic accessor that can create and change values of any supported datatype, as demonstrated in this example.
Incrementing node values
Iris.increment() takes an integer number argument, increments the node value by that amount, and returns the incremented value. The initial target node value can be any supported numeric type, but the incremented value will be an integer. If there is no node at the target address, the method creates one and assigns the number argument as the value. This method uses a thread-safe atomic operation to change the value of the node, so the node is never locked.
In the following example, the first call to increment() creates new subnode myGlobal('B') with value -2, and assigns the returned value to total. The next two calls each increment by -2 and assign the new value to total, and the loop exits when the node value is -6.
  do {
    var total = native.increment(-2,'myGlobal', 'B');
  } while (total > -6)
  console.log('total = ' + total + ' and myGlobal('B') = ' + native.get('myGlobal', 'B'))

// Prints: total = -6 and myGlobal('B') = -6

Note:
—Naming rules—
Naming rules
Global names and subscripts obey the following rules:
  • The length of a node address (totaling the length of the global name and all subscripts) can be up to 511 characters. (Some typed characters may count as more than one encoded character for this limit. For more information, see “Maximum Length of a Global Reference” in Using Globals).
  • A global name can include letters, numbers, and periods ('.'), and can have a length of up to 31 significant characters. It must begin with a letter, and must not end with a period.
  • A subscript can be a string, an integer, or a number. String subscripts are case-sensitive, and can include characters of all types. Length is limited only by the 511 character maximum for the total node address.
Deleting a node or group of nodes
Iris.kill() — deletes the specified node and all of its subnodes. The entire global array will be deleted if the root node is deleted or if all nodes with values are deleted.
Global array myGlobal initially contains the following nodes:
   myGlobal = <valueless node>
     myGlobal('A') = 0
       myGlobal('A',1) = 0
       myGlobal('A',2) = 0
     myGlobal('B') = <valueless node>
       myGlobal('B',1) = 0

This example will delete the global array by calling kill() on two of its subnodes. The first call will delete node myGlobal('A') and both of its subnodes:
  native.kill('myGlobal', 'A');
  // also kills child nodes myGlobal('A',1) and myGlobal('A',2)
The second call deletes the last remaining subnode with a value, killing the entire global array:
  native.kill('myGlobal','B',1);
  • The parent node, myGlobal('B'), is deleted because it is valueless and now has no subnodes.
  • Root node myGlobal is valueless and now has no subnodes, so the entire global array is deleted from the database.
The entire global array could have been deleted immediately by calling kill() on the root node:
  native.kill('myGlobal');

Finding Nodes in a Global Array

The Native API provides ways to iterate over part or all of a global array. The following topics describe the various iteration methods:

Iterating Over a Set of Child Nodes

Child nodes are sets of nodes immediately under the same parent node. Any child node address can be defined by appending one subscript to the subscript list of the parent. For example, the following global array has four child nodes under parent node heros('dogs'):
The heros global array
This global array uses the names of several heroic dogs (plus a reckless boy and a pioneering sheep) as subscripts. The values are birth years.
  heros                                           // root node,    valueless, 2 child nodes
     heros('dogs')                                // level 1 node, valueless, 4 child nodes
        heros('dogs','Balto') = 1919              // level 2 node, value=1919
        heros('dogs','Hachiko') = 1923            // level 2 node, value=1923
        heros('dogs','Lassie') = 1940             // level 2 node, value=1940, 1 child node
           heros('dogs','Lassie','Timmy') = 1954  // level 3 node, value=1954
        heros('dogs','Whitefang') = 1906          // level 2 node, value=1906
     heros('sheep')                               // level 2 node, valueless, 1 child node
        heros('sheep','Dolly') = 1996             // level 2 node, value=1996

The following methods are used to create an iterator, define the direction of iteration, and set the starting point of the search:
  • Iris.iterator() returns an instance of Iterator for the child nodes of the specified target node.
  • Iterator.reversed() — toggles iteration between forward and reverse collation order.
  • Iterator.startFrom() sets the iterator's starting position to the specified subscript. The subscript is an arbitrary starting point, and does not have to address an existing node.
Read child node values in reverse order
The following code iterates over child nodes of heros('dogs') in reverse collation order, starting with subscript V:
// Create a reverse iterator for child nodes of heros('dogs')
  let iterDogs = native.iterator('heros','dogs').reversed().startFrom('V');

  let output = '\nDog birth years: ';
  for ([key,value] of iterDogs) {
    output += key + ':' + value + '  ';
  };
  console.log(output);

This code prints the following output:
  Dog birth years: Lassie:1940  Hachiko:1923  Balto:1919
In this example, two subnodes of heros('dogs') are ignored:
  • Child node heros('dogs','Whitefang') will not be found because it is outside of the search range (Whitefang is higher than V in collation order).
  • Level 3 node heros('dogs','Lassie','Timmy') will not be found because it is a child of Lassie, not dogs.
See the last section in this chapter (“Testing for Child Nodes and Node Values”) for a discussion of how to iterate over multiple node levels.
Note:
Collation Order
The order in which nodes are retrieved depends on the collation order of the subscripts. When a node is created, it is automatically stored it in the collation order specified by the storage definition. In this example, the child nodes of heros('dogs') would be stored in the order shown (Balto, Hachiko, Lassie, Whitefang) regardless of the order in which they were created. For more information, see “Collation of Global Nodes” in Using Globals.

Iteration with next()

The Native API also supports the standard next() and return type iterator methods:
  • Iterator.next() — positions the iterator at the next child node (if one exists) and returns an array containing properties done and value. The done property will be false if there are no more nodes. When an iterator is created, it defaults to the entries() return type.
  • Iterator.entries() — sets return type to an array containing both the key (the top level subscript of the child node) and the node value. For example, the returned value for node heros(,'dogs','Balto') would be ['Balto',1919].
  • Iterator.keys() — sets return type to return only the key (the top level subscript).
  • Iterator.values() — sets return type to return only the node value.
In the following example, each call to the next() method sets variable iter to an array containing the current values for the iterator done and value properties. Since the keys() method was called when the iterator was created, the value property will contain only the key (top level subscript) for the current child node of heros('dogs').
Use next() to list the subscripts under node heros('dogs')
// Get a list of child subscripts under node heros('dogs')
  let iterDogs = native.iterator('heros','dogs').keys();
  let output = "\nSubscripts under node heros('dogs'): ";

  let iter = iterDogs.next();
  while (!iter.done) {
    output += iter.value + ' ';
    iter = iterDogs.next();
  }
  console.log(output);

This code prints the following output:
  Subscripts under node heros('dogs'): Balto Hachiko Lassie Whitefang

Testing for Child Nodes and Node Values

In the previous examples, the scope of the search is restricted to child nodes of heros('dogs'). The iterator fails to find two values in global array heros because they are under different parents:
  • Level 3 node heros('dogs','Lassie','Timmy') will not be found because it is a child of Lassie, not dogs.
  • Level 2 node heros('sheep','Dolly') is not found because it is a child of sheep, not dogs.
To search the entire global array, we need to find all of the nodes that have child nodes, and create an iterator for each set of child nodes. The isDefined() method provides the necessary information:
  • Iris.isDefined() — can be used to determine if a node has a value, a subnode, or both. It returns one of the following values:
    • 0 — the specified node does not exist
    • 1 — the node exists and has a value
    • 10 — the node is valueless but has a child node
    • 11 — the node has both a value and a child node
    The returned value can be used to determine several useful boolean values:
       let exists = (native.isDefined(root,subscripts) > 0);      // value is 1, 10, or 11
       let hasValue = (native.isDefined(root,subscripts)%10 > 0); // value is 1 or 11
       let hasChild = (native.isDefined(root,subscripts) > 9);    // value is 10 or 11
    
    
The following example consists of two methods:
  • findAllHeros() iterates over child nodes of the current node, and calls testNode() for each node. Whenever testNode() indicates that the current node has child nodes, findAllHeros() creates a new iterator for the next level of child nodes.
  • testNode() will be called for each node in the heros global array. It calls isDefined() on the current node, and returns a boolean value indicating whether the node has child nodes. It also prints node information for each node.
Method findAllHeros()
This example processes a known structure, and traverses the various levels with simple nested calls. In the less common case where a structure has an arbitrary number of levels, a recursive algorithm could be used.
  function findAllHeros() {
    const root = 'heros';
    console.log('List all subnodes of root node '+root+':\n'+root)
    let iterRoot = native.iterator(root);
    let hasChild = false;

    // Iterate over children of root node heros
    for ([sub1,value] of iterRoot) {
      hasChild = testNode(value,root,sub1);

      // Iterate over children of heros(sub1)
      if (hasChild) {
        let iterOne = native.iterator(root,sub1);
        for ([sub2,value] of iterOne) {
          hasChild = testNode(value,root,sub1,sub2);

          // Iterate over children of heros(sub1,sub2)
          if (hasChild) {
            let iterTwo = native.iterator(root,sub1,sub2);
            for ([sub3,value] of iterTwo) {
              testNode(value,root,sub1,sub2,sub3); //no child nodes below level 3
            }
          } //end level 2
        }
      } //end level 1
    } // end main loop
  } // end findAllHeros()

Method testNode()
  function testNode(value, root, ...subs) {

  // Test for values and child nodes
    let state = native.isDefined(root,...subs);
    let hasValue = (state%10 > 0); // has value if state is 1 or 11
    let hasChild = (state > 9);    // has child if state is 10 or 11

  // format the node address output string
    let subList = Array.from(subs);
    let level = subList.length-1;
    let indent = '  ' + String('      ').slice(0,(level*2));
    let address = indent + root+'(' + subList.join() + ')';

  // Add node value to string and note special cases
    if (hasValue) { // ignore valueless nodes
      address += ' = ' + value;
      for (name of ['Timmy','Dolly']) {
        if (name == subList[level]) {
          address += ' (not a dog!)'
        }
      }
    }
    console.log(address);
    return hasChild;
  }
}
This method will write the following lines:
List all subnodes of root node heros:
heros
  heros(dogs)
    heros(dogs,Balto) = 1919
    heros(dogs,Hachiko) = 1923
    heros(dogs,Lassie) = 1940
      heros(dogs,Lassie,Timmy) = 1954  (not a dog!)
    heros(dogs,Whitefang) = 1906
  heros(sheep)
    heros(sheep,Dolly) = 1996  (not a dog!)

The output of testNodes() includes some nodes that were not found in previous examples because they are not child nodes of heros('dogs'):
  • heros('dogs','Lassie','Timmy') is a child of Lassie, not dogs.
  • heros('sheep','Dolly') is a child of sheep, not dogs.

Transactions and Locking

The following topics are discussed in this section:

Controlling Transactions

The Native API provides the following methods to control transactions:
  • Iris.tCommit() — commits one level of transaction.
  • Iris.tStart() — starts a transaction (which may be a nested transaction).
  • Iris.getotalevel() — returns an int value indicating the current transaction level (0 if not in a transaction).
  • Iris.tRollback() — rolls back all open transactions in the session.
  • Iris.tRollbackOne() — rolls back the current level transaction only. If this is a nested transaction, any higher-level transactions will not be rolled back.
The following example starts three levels of nested transaction, setting the value of a different node in each transaction level. All three nodes are printed to prove that they have values. The example then rolls back the second and third levels and commits the first level. All three nodes are printed again to prove that only the first node still has a value.
Controlling Transactions: Using three levels of nested transaction
  const node = 'myGlobal';
  console.log('Set three values in three different transaction levels:');
  for (let i=1; i<4; i++) {
    native.tStart();
    let lvl = native.getTLevel()
    native.set(('Value'+lvl), node, lvl);
    let val = '<valueless>'
    if (native.isDefined(node,lvl)%10 > 0) val = native.get(node,lvl);
    console.log('  ' + node + '(' + i + ') = ' + val + ' (tLevel is ' + lvl + ')');
  }
// Prints: Set three values in three different transaction levels:
//         myGlobal(1) = Value1 (tLevel is 1)
//         myGlobal(2) = Value2 (tLevel is 2)
//         myGlobal(3) = Value3 (tLevel is 3)

  console.log('Roll back two levels and commit the level 1 transaction:');
  let act = ['  tRollbackOne','  tRollbackOne','       tCommit'];
  for (let i=3; i>0; i--) {
    if (i>1) {native.tRollbackOne();} else {native.tCommit();}
    let val = '<valueless>'
    if (native.isDefined(node,i)%10 > 0) val = native.getString(node,i);
    console.log(act[3-i]+' (tLevel='+native.getTLevel()+'): '+node+'('+i+') = '+val);
  }

// Prints: Roll back two levels and commit the level 1 transaction:
//           tRollbackOne (tLevel=2): myGlobal(3) = <valueless>
//           tRollbackOne (tLevel=1): myGlobal(2) = <valueless>
//                tCommit (tLevel=0): myGlobal(1) = Value1

Acquiring and Releasing Locks

The following methods of class Iris are used to acquire and release locks. Both methods take a lockMode argument to specify whether the lock is shared or exclusive:
  • Iris.lock() — Takes lockMode, timeout, globalName, and subscripts arguments, and locks the node. The lockMode argument specifies whether any previously held locks should be released. This method will time out after a predefined interval if the lock cannot be acquired.
  • Iris.unlock() — Takes lockMode, globalName, and subscripts arguments, and releases the lock on a node.
The following argument values can be used:
  • lockMode — combination of the following chars, S for shared lock, E for escalating lock, default is empty string (exclusive and non-escalating)
  • timeout — amount to wait to acquire the lock in seconds
Note:
You can use the Management Portal to examine locks. Go to System Operation > Locks to see a list of the locked items on your system.

Using Locks in a Transaction

This section demonstrates incremental locking within a transaction, using the methods previously described (see “Controlling Transactions” and “Acquiring and Releasing Locks”). You can see a list of the locked items on your system by opening the Management Portal and going to System Operation > Locks. The calls to alert() in the following code will pause execution so that you can look at the list whenever it changes.
There are two ways to release all currently held locks:
  • Iris.releaseAllLocks() — releases all locks currently held by this connection.
  • When the close() method of the connection object is called, it releases all locks and other connection resources.
The following examples demonstrate the various lock and release methods.
Using incremental locking in transactions
  native.set('exclusive node','nodeOne');
  native.set('shared node','nodeTwo');

// unlike global names, lock references *must* start with circumflex
  const nodeOneRef = '^nodeOne';
  const nodeTwoRef = '^nodeTwo';

  try {
    native.tStart();
    native.lock('E',10,nodeOneRef,''); // lock nodeOne exclusively
    native.lock('S',10,nodeTwoRef,''); // lock nodeTwo shared
    console.log('Exclusive lock on nodeOne and shared lock on nodeTwo');

    alert('Press return to release locks individually');
    native.unlock('D',nodeOneRef,''); // release nodeOne after transaction
    native.unlock('I',nodeTwoRef,''); // release nodeTwo immediately

    alert('Press return to commit transaction');
    native.tCommit();
  }
  catch { console.log('error'); }

Using non-incremental locking in transactions
// lock nodeOne non-incremental, nodeTwo shared non-incremental
  native.lock('',10,nodeOneRef,'');

  alert('Exclusive lock on nodeOne, return to lock nodeOne non-incrementally');
  native.lock('S',10,nodeTwoRef,'');

  alert('Verify that only nodeTwo is now locked, then press return');

Using releaseAllLocks() in transactions to release all incremental locks
// lock nodeOne shared incremental, nodeTwo exclusive incremental
  native.lock('SE',10,nodeOneRef,'');
  native.lock('E',10,nodeTwoRef,'');

  alert('Two locks are held (one with lock count 2), return to release both locks');
  native.releaseAllLocks();

  alert('Verify both locks have been released, then press return');

Previous section   Next section