"use strict";
/**
* @module opcua.client
*/
const async = require("async");
const assert = require("node-opcua-assert").assert;
const _ = require("underscore");
const util = require("util");
const EventEmitter = require("events").EventEmitter;
const read_service = require("node-opcua-service-read");
const call_service = require("node-opcua-service-call");
const AttributeIds = require("node-opcua-data-model").AttributeIds;
const AccessLevelFlag = require("node-opcua-data-model").AccessLevelFlag;
const makeResultMask = require("node-opcua-data-model").makeResultMask;
const BrowseDirection = require("node-opcua-data-model").BrowseDirection;
const NodeClass = require("node-opcua-data-model").NodeClass;
const makeNodeClassMask = require("node-opcua-data-model").makeNodeClassMask;
const ReferenceTypeIds = require("node-opcua-constants").ReferenceTypeIds;
const ObjectTypeIds = require("node-opcua-constants").ObjectTypeIds;
const NodeId = require("node-opcua-nodeid").NodeId;
const makeNodeId = require("node-opcua-nodeid").makeNodeId;
const coerceNodeId = require("node-opcua-nodeid").coerceNodeId;
const StatusCodes = require("node-opcua-status-code").StatusCodes;
const lowerFirstLetter = require("node-opcua-utils").lowerFirstLetter;
const Variant = require("node-opcua-variant").Variant;
const VariantArrayType = require("node-opcua-variant").VariantArrayType;
const DataType = require("node-opcua-variant").DataType;
const ClientSession = require("node-opcua-client").ClientSession;
const ClientSubscription = require("node-opcua-client").ClientSubscription;
const resultMask = makeResultMask("ReferenceType | IsForward | BrowseName | NodeClass | TypeDefinition");
function makeRefId(referenceTypeName) {
const nodeId = makeNodeId(ReferenceTypeIds[referenceTypeName] || ObjectTypeIds[referenceTypeName]);
// istanbul ignore next
if (nodeId.isEmpty()) {
throw new Error("makeRefId: cannot find ReferenceTypeName + ", referenceTypeName);
}
return nodeId;
}
exports.makeRefId = makeRefId;
/**
* @method convertNodeIdToDataTypeAsync
*
* @param session {ClientSession}
* @param dataTypeId {NodeId}
* @param callback {Function}
* @param callback.err {Error|null}
* @param callback.dataType {DataType}
*
* @example
*
* var dataTypeId ="ns=0;i=11"; // Double
* convertNodeIdToDataTypeAsync(session,dataTypeId,function(err,dataType) {
* assert(!err && dataType === DataType.Double);
* });
*
* var dataTypeId ="ns=0;i=290"; // Duration => SubTypeOf Double
* convertNodeIdToDataTypeAsync(session,dataTypeId,function(err,dataType) {
* assert(!err && dataType === DataType.Double);
* });
*
* see also AddressSpace#findCorrespondingBasicDataType
*/
function convertNodeIdToDataTypeAsync(session, dataTypeId, callback) {
const nodeToRead = {
nodeId: dataTypeId,
attributeId: AttributeIds.BrowseName
};
session.read(nodeToRead, function (err, dataValue) {
// istanbul ignore next
if (err) {
return setImmediate(function(){callback(err);});
}
let dataType;
// istanbul ignore next
if (dataValue.statusCode !== StatusCodes.Good) {
//Xx console.log("convertNodeIdToDataTypeAsync: Cannot read browse name for nodeID ".red + dataTypeId.toString());
dataType = DataType.Null;
return setImmediate(function(){callback(null, dataType);});
}
const dataTypeName = dataValue.value.value;
if (dataTypeId.namespace === 0 && DataType.get(dataTypeId.value)) {
dataType = DataType.get(dataTypeId.value);
return setImmediate(function(){callback(null, dataType);});
}
/// example => Duration (i=290) => Double (i=11)
// read subTypeOf
const nodeToBrowse = {
// BrowseDescription
referenceTypeId: makeRefId("HasSubtype"),
//xx nodeClassMask: makeNodeClassMask("ObjectType"),
includeSubtypes: false,
browseDirection: BrowseDirection.Inverse,
nodeId: dataTypeId,
resultMask: resultMask
};
session.browse(nodeToBrowse, function (err, browseResult) {
// istanbul ignore next
if (err) {
return callback(err);
}
const references = browseResult.references;
if (!references || references.length !== 1) {
return callback(new Error("cannot find SuperType of " + dataTypeName.toString()));
}
const nodeId = references[0].nodeId;
return convertNodeIdToDataTypeAsync(session, nodeId, callback);
});
});
}
function convertNodeIdToDataType(dataTypeId) {
return dataTypeId._dataType;
}
function ProxyBaseNode(proxyManager, nodeId) {
const self = this;
/**
* the object nodeId
* @property nodeId
* @type {NodeId}
*/
self.nodeId = nodeId;
self.proxyManager = proxyManager;
assert(self.proxyManager.session, "expecting valid session");
Object.defineProperty(self, "proxyManager", {
enumerable: false,
writable: true
});
/**
* the object's components
* @property $components
* @type {Array<ProxyBaseNode>}
*/
self.$components = [];
/**
* the object's properties
* @property $properties
* @type {Array<ProxyBaseNode>}
*/
self.$properties = [];
/**
* the object's properties
* @property $methods
* @type {Array<ProxyBaseNode>}
*/
self.$methods = [];
/**
* the Folder's elements
* @property $organizes
* @type {Array<ProxyBaseNode>}
*/
self.$organizes = [];
/**
* the object's description
* @property description
* @type {String}
*/
self.description = "";
/**
* the object's browseName
* @property browseName
* @type {String}
*/
self.browseName = "";
/**
* the object's NodeClass
* @property nodeClass
* @type {NodeClass}
*/
self.nodeClass = null;
}
util.inherits(ProxyBaseNode, EventEmitter);
/**
* get a updated Value of the Variable , by using a ReadRequest
* @method readValue
* @param callback {Function}
* @param callback.err {Error|null}
* @param callback.dataValue {DataValue}
*/
ProxyBaseNode.prototype.readValue = function (callback) {
const self = this;
assert(self.proxyManager);
const session = self.proxyManager.session;
assert(session);
const nodeToRead = {
nodeId: self.nodeId,
attributeId: AttributeIds.Value
};
self.proxyManager.session.read(nodeToRead, function (err, dataValue) {
// istanbul ignore next
if (err) {
return callback(err);
}
const data = dataValue.value;
callback(null, data);
});
};
/**
* set the Value of the Variable, by using a WriteRequest
* @method writeValue
* @param dataValue {DataValue}
* @param callback {Function}
* @param callback.err {Error|null}
*/
ProxyBaseNode.prototype.writeValue = function (dataValue, callback) {
const self = this;
assert(self.proxyManager);
const session = self.proxyManager.session;
assert(session);
const nodeToWrite = {
nodeId: self.nodeId,
attributeId: AttributeIds.Value,
value: dataValue
};
self.proxyManager.session.write(nodeToWrite, function (err, statusCode) {
// istanbul ignore next
if (err) {
return callback(err);
}
if (statusCode !== StatusCodes.Good) {
callback(new Error(statusCode.toString()));
} else {
callback(null);
}
});
};
ProxyBaseNode.prototype.toString = function () {
const str = [];
const self = this;
str.push(" ProxyObject ");
str.push(" browseName : " + self.browseName.toString());
str.push(" typeDefinition : " + self.typeDefinition.toString());
str.push(" $components# : " + self.$components.length.toString());
str.push(" $properties# : " + self.$properties.length.toString());
return str.join("\n");
};
function ProxyVariable(session, nodeId) {
ProxyBaseNode.apply(this, arguments);
}
util.inherits(ProxyVariable, ProxyBaseNode);
const ProxyObject = ProxyVariable;
function ObjectExplorer(options) {
const self = this;
self.proxyManager = options.proxyManager;
self.name = options.name;
self.nodeId = options.nodeId;
self.parent = options.parent;
}
ObjectExplorer.prototype.$resolve = function (callback) {
const self = this;
self.proxyManager.getObject(self.nodeId, function (err, childObj) {
// istanbul ignore next
if (err) {
return callback(err);
}
self.parent[self.name] = childObj;
self.parent.$components.push(childObj);
callback(null)
});
};
function readUAStructure(proxyManager, obj, callback) {
const session = proxyManager.session;
// 0 Object
// 1 Variable
// 2 Method
const nodeId = obj.nodeId;
const nodesToBrowse = [
// Components (except Methods)
{
// BrowseDescription
referenceTypeId: makeRefId("HasComponent"),
nodeClassMask: makeNodeClassMask("Object | Variable"), // we don't want Method here
includeSubtypes: true,
browseDirection: BrowseDirection.Forward,
nodeId: nodeId,
resultMask: resultMask
},
// Properties
{
// BrowseDescription
referenceTypeId: makeRefId("HasProperty"),
//nodeClassMask: makeNodeClassMask("Variable"),
includeSubtypes: true,
browseDirection: BrowseDirection.Forward,
nodeId: nodeId,
resultMask: resultMask
},
// Methods
{
// BrowseDescription
referenceTypeId: makeRefId("HasComponent"),
nodeClassMask: makeNodeClassMask("Method"),
includeSubtypes: true,
browseDirection: BrowseDirection.Forward,
nodeId: nodeId,
resultMask: resultMask
},
// TypeDefinition
{
// BrowseDescription
referenceTypeId: makeRefId("HasTypeDefinition"),
includeSubtypes: true,
browseDirection: BrowseDirection.Both,
nodeId: nodeId,
resultMask: resultMask
},
// FromState
{
// BrowseDescription
referenceTypeId: makeRefId("FromState"),
includeSubtypes: true,
browseDirection: BrowseDirection.Forward,
nodeId: nodeId,
resultMask: resultMask
},
// ToState
{
// BrowseDescription
referenceTypeId: makeRefId("ToState"),
includeSubtypes: true,
browseDirection: BrowseDirection.Forward,
nodeId: nodeId,
resultMask: resultMask
},
// (for folders ) Organizes
{
// BrowseDescription
referenceTypeId: makeRefId("Organizes"),
includeSubtypes: true,
browseDirection: BrowseDirection.Forward,
nodeId: nodeId,
resultMask: resultMask
}
];
/**
* @method add_method
* construct a callable method
*
* @param obj
* @param reference
* @param callback
* @private
*/
function add_method(obj, reference, callback) {
const name = lowerFirstLetter(reference.browseName.name);
obj[name] = function functionCaller(inputArgs, callback) {
assert(_.isFunction(callback));
// convert input arguments into Variants
const inputArgsDef = obj[name].inputArguments || [];
const inputArguments = inputArgsDef.map(function (arg) {
const dataType = convertNodeIdToDataType(arg.dataType);
const arrayType = (arg.valueRank === 1) ? VariantArrayType.Array : VariantArrayType.Scalar;
//xx console.log("xxx ",arg.toString());
const propName = lowerFirstLetter(arg.name);
const value = inputArgs[propName];
if (value === undefined) {
throw new Error("expecting input argument " + propName);
}
if (arrayType === VariantArrayType.Array) {
if (!_.isArray(value)) {
throw new Error("expecting value to be an Array or a TypedArray");
}
}
return new Variant({arrayType: arrayType, dataType: dataType, value: value});
});
const methodToCall = new call_service.CallMethodRequest({
objectId: obj.nodeId,
methodId: reference.nodeId,
inputArguments: inputArguments
});
//xx console.log(" calling ",methodToCall.toString());
session.call(methodToCall, function (err, callResult) {
// istanbul ignore next
if (err) {
return callback(err);
}
if (callResult.statusCode !== StatusCodes.Good) {
return callback(new Error("Error " + callResult.statusCode.toString()));
}
callResult.outputArguments = callResult.outputArguments || [];
obj[name].outputArguments = obj[name].outputArguments || [];
assert(callResult.outputArguments.length === obj[name].outputArguments.length);
const outputArgs = {};
const outputArgsDef = obj[name].outputArguments;
_.zip(outputArgsDef, callResult.outputArguments).forEach(function (pair) {
const arg = pair[0];
const variant = pair[1];
const propName = lowerFirstLetter(arg.name);
outputArgs[propName] = variant.value;
});
callback(err, outputArgs);
});
};
function extractDataType(arg, callback) {
if (arg.dataType && arg.dataType._dataType) {
return setImmediate(callback); // already converted
}
convertNodeIdToDataTypeAsync(session, arg.dataType, function (err, dataType) {
if (!err) {
assert(dataType.hasOwnProperty("value"));
arg.dataType._dataType = dataType;
}
callback(err);
});
}
const methodObj = {
nodeId: reference.nodeId,
executableFlag: false,
browseName: name,
func: obj[name]
};
obj.$methods[name] = methodObj;
async.parallel([
function(callback) {
session.getArgumentDefinition(reference.nodeId, function (err, args) {
// istanbul ignore next
if (err) {
return setImmediate(function() { callback(err); });
}
const inputArguments = args.inputArguments;
const outputArguments = args.outputArguments;
obj[name].inputArguments = inputArguments;
obj[name].outputArguments = outputArguments;
async.series([
function (callback) {
async.each(obj[name].inputArguments, extractDataType, callback);
},
function (callback) {
async.each(obj[name].outputArguments, extractDataType, callback);
}
], callback)
});
},
function(callback) {
proxyManager._monitor_execution_flag(methodObj, function () {
callback();
});
}
],callback);
}
function add_component(obj, reference, callback) {
const name = lowerFirstLetter(reference.browseName.name || "");
proxyManager.getObject(reference.nodeId, function (err, childObj) {
// istanbul ignore else
if (!err) {
childObj = new ObjectExplorer({
proxyManager: proxyManager,
nodeId: reference.nodeId,
name: name,
parent: obj
});
obj[name] = childObj;
obj.$components.push(childObj);
childObj.$resolve(callback);
} else {
callback(err);
}
});
}
function addFolderElement(obj, reference, callback) {
const name = lowerFirstLetter(reference.browseName.name || "");
const childObj = new ObjectExplorer({
proxyManager: proxyManager,
nodeId: reference.nodeId,
name: name,
parent: obj
});
obj[name] = childObj;
obj.$organizes.push(childObj);
childObj.$resolve(callback);
}
function add_property(obj, reference, callback) {
const name = lowerFirstLetter(reference.browseName.name || "");
obj[name] = new ProxyVariable(proxyManager, reference.nodeId, reference);
obj.$properties[name] = obj[name];
setImmediate(callback);
}
function add_typeDefinition(obj, references, callback) {
references = references || [];
if (references.length !== 1) {
//xx console.log(" cannot find type definition", references.length);
return setImmediate(callback);
}
const reference = references[0];
assert(!obj.typeDefinition, "type definition can only be set once");
obj.typeDefinition = reference.browseName.name || "";
setImmediate(callback);
}
function addFromState(obj, reference, callback) {
proxyManager.getObject(reference.nodeId, function (err, childObj) {
obj.$fromState = childObj;
callback(err);
});
}
function addToState(obj, reference, callback) {
proxyManager.getObject(reference.nodeId, function (err, childObj) {
obj.$toState = childObj;
callback(err);
});
}
session.browse(nodesToBrowse, function (err, browseResults) {
function t(references) {
return references.map(function (r) {
return r.browseName.name + " " + r.nodeId.toString();
});
}
// istanbul ignore next
if (err) {
return callback(err);
}
//xx console.log("Components", t(results[0].references));
//xx console.log("Properties", t(results[1].references));
//xx console.log("Methods", t(results[2].references));
async.parallel([
function (callback) {
async.map(browseResults[0].references, add_component.bind(null, obj), callback);
},
function (callback) {
async.map(browseResults[1].references, add_property.bind(null, obj), callback);
},
// now enrich our object with nice callable async methods
function (callback) {
async.map(browseResults[2].references, add_method.bind(null, obj), callback);
},
// now set typeDefinition
function (callback) {
add_typeDefinition.bind(null, obj)(browseResults[3].references, callback);
},
//
function (callback) { // FromState
// fromState
const reference = browseResults[4].references ? browseResults[4].references[0] : null;
// fromState
if (reference) {
return addFromState(obj, reference, callback);
}
callback();
},
function (callback) { // ToState
const reference = browseResults[5].references ? browseResults[5].references[0] : null;
// fromState
if (reference) {
return addToState(obj, reference, callback);
}
callback();
},
function (callback) { // Organizes
async.map(browseResults[6].references, addFolderElement.bind(null, obj), callback);
}
], callback);
});
}
/**
* @method getObject
*
* @param proxyManager
* @param nodeId
* @param options
* @param callback {Function}
* @param callback.err
* @param callback.clientObject
*
*/
function getObject(proxyManager, nodeId, options, callback) {
const session = proxyManager.session;
nodeId = coerceNodeId(nodeId);
if (nodeId.isEmpty()) {
return setImmediate(function() {
callback(new Error(" Invalid empty node in getObject"));
});
}
const nodesToRead = [
{
nodeId: nodeId,
attributeId: AttributeIds.BrowseName
},
{
nodeId: nodeId,
attributeId: AttributeIds.Description
},
{
nodeId: nodeId,
attributeId: AttributeIds.NodeClass
}
];
function read_accessLevels(clientObject, callback) {
const nodesToRead = [
{
nodeId: nodeId,
attributeId: AttributeIds.Value
},
{
nodeId: nodeId,
attributeId: AttributeIds.UserAccessLevel
},
{
nodeId: nodeId,
attributeId: AttributeIds.AccessLevel
}
];
session.read(nodesToRead, 1, function (err, dataValues) {
if (dataValues[0].statusCode === StatusCodes.Good) {
clientObject.dataValue = AccessLevelFlag.get(dataValues[0].value);
}
if (dataValues[1].statusCode === StatusCodes.Good) {
//xx console.log("AccessLevel ", results[3].value.toString())
clientObject.userAccessLevel = AccessLevelFlag.get(dataValues[1].value.value);
}
if (dataValues[2].statusCode === StatusCodes.Good) {
clientObject.accessLevel = AccessLevelFlag.get(dataValues[2].value.value);
}
callback(err);
});
}
let clientObject;
async.series([
function (callback) {
// readAttributes like browseName and references
session.read(nodesToRead, 1, function (err, dataValues) {
if (!err) {
if (dataValues[0].statusCode === StatusCodes.BadNodeIdUnknown) {
//xx console.log(" INVALID NODE ", nodeId.toString());
return callback(new Error("Invalid Node " + nodeId.toString()));
}
clientObject = new ProxyObject(proxyManager, nodeId);
///x console.log("xxxx ,s",results.map(function(a){ return a.toString();}));
clientObject.browseName = dataValues[0].value.value;
clientObject.description = (dataValues[1].value ? dataValues[1].value.value : "");
clientObject.nodeClass = NodeClass.get(dataValues[2].value.value);
//xx console.log("xxx nodeClass = ",clientObject.nodeClass.toString());
if (clientObject.nodeClass === NodeClass.Variable) {
return read_accessLevels(clientObject, callback);
}
}
callback(err);
});
},
function (callback) {
// install monitored item
if (clientObject.nodeClass === NodeClass.Variable) {
//xx console.log("xxxx -> ???", clientObject.nodeId.toString(), clientObject.nodeClass.toString());
return proxyManager._monitor_value(clientObject, callback);
}
callback();
},
function (callback) {
readUAStructure(proxyManager, clientObject, callback);
}
//
], function (err) {
// istanbul ignore next
if (err) {
return callback(err);
}
callback(null, clientObject);
});
}
/**
* @class UAProxyManager
* @param session
* @constructor
*/
function UAProxyManager(session) {
const self = this;
self.session = session;
assert(session instanceof ClientSession);
self._map = {};
// create a subscription
}
/**
* @class ClientSession
* @method createSubscription2
* @param createSubscriptionRequest
* @param callback
* @param callback.err
* @param callback.subscription
*
*
* subscription.on("error', function(err){ ... });
* subscription.on("terminate',function(err){ ... });
* var monitoredItem = subscription.monitor(itemToMonitor,monitoringParameters,requestedParameters,callback);
* monitoredItem.on("changed",function( dataValue) {...});
*
*/
ClientSession.prototype.createSubscription2 = function (createSubscriptionRequest, callback) {
assert(_.isFunction(callback));
const self = this;
const subscription = new ClientSubscription(self, createSubscriptionRequest);
subscription.on("error", function (err) {
});
subscription.on("started", function () {
callback(null, subscription);
});
};
/**
* @class UAProxyManager
* @method start
* @param callback
* @async
*/
UAProxyManager.prototype.start = function (callback) {
const self = this;
const createSubscriptionRequest = {
requestedPublishingInterval: 100,
requestedLifetimeCount: 6000,
requestedMaxKeepAliveCount: 100,
maxNotificationsPerPublish: 1000,
publishingEnabled: true,
priority: 10
};
self.session.createSubscription2(createSubscriptionRequest, function (err, subscription) {
self.subscription = subscription;
self.subscription.on("terminated", function () {
self.subscription = null;
});
callback(err);
});
};
/**
* @method stop
* @param callback
* @async
*/
UAProxyManager.prototype.stop = function (callback) {
const self = this;
if (self.subscription) {
self.subscription.terminate(function() {
self.subscription = null;
callback();
});
} else {
callback(new Error("UAProxyManager already stopped ?"))
}
};
/**
* @method getObject
* @param nodeId
* @param callback
* @async
*/
UAProxyManager.prototype.getObject = function (nodeId, callback) {
let options = {};
const self = this;
setImmediate(function () {
options = options || {};
options.depth = options.depth || 1;
const key = nodeId.toString();
if (self._map.hasOwnProperty(key)) {
return callback(null, self._map[key]);
}
getObject(self, nodeId, options, function (err, obj) {
if (!err) {
self._map[key] = obj;
}
callback(err, obj);
});
});
};
UAProxyManager.prototype._monitor_value = function (proxyObject, callback) {
assert(_.isFunction(callback));
//xx if (proxyObject.nodeId.toString() !== "ns=0;i=2257") { return;}
const self = this;
if (!self.subscription) {
// some server do not provide subscription support, do not treat this as an error.
return callback(null); // new Error("No subscription"));
}
const itemToMonitor = { // ReadValueId
nodeId: proxyObject.nodeId,
attributeId: AttributeIds.Value
};
const monitoringParameters = { // MonitoringParameters
samplingInterval: 0, /* event-based */
discardOldest: true,
queueSize: 10
};
const requestedParameters = read_service.TimestampsToReturn.Both;
const monitoredItem = self.subscription.monitor(itemToMonitor, monitoringParameters, requestedParameters, callback);
// console.log("xxxxxx installing monitored Item",monitoredItem.itemToMonitor.nodeId.toString(),monitoredItem.itemToMonitor.attributeId);
//xx proxyObject.monitoredItem = monitoredItem;
Object.defineProperty(proxyObject, "__monitoredItem", {value: monitoredItem, enumerable: false});
proxyObject.__monitoredItem.on("changed", function (dataValue) {
proxyObject.dataValue = dataValue;
proxyObject.emit("value_changed", dataValue);
//xx console.log("xxx Value Changed ".red,proxyObject.nodeId.toString() , proxyObject.browseName,proxyObject.dataValue.toString());
});
};
UAProxyManager.prototype._monitor_execution_flag = function (proxyObject, callback) {
// note : proxyObject must wrap a method
assert(_.isFunction(callback));
assert(proxyObject.nodeId instanceof NodeId);
const self = this;
if (!self.subscription) {
// some server do not provide subscription support, do not treat this as an error.
return callback(null); // new Error("No subscription"));
}
const itemToMonitor = { // ReadValueId
nodeId: proxyObject.nodeId,
attributeId: AttributeIds.Executable
};
const monitoringParameters = { // MonitoringParameters
samplingInterval: 0, /* event-based */
discardOldest: true,
queueSize: 10
};
const requestedParameters = read_service.TimestampsToReturn.None;
const monitoredItem = self.subscription.monitor(itemToMonitor, monitoringParameters, requestedParameters, callback);
Object.defineProperty(proxyObject, "__monitoredItem_execution_flag", {value: monitoredItem, enumerable: false});
proxyObject.__monitoredItem_execution_flag.on("changed", function (dataValue) {
proxyObject.executableFlag = dataValue.value.value;
//xx console.log(" execution flag = ", proxyObject.executableFlag , proxyObject.browseName , proxyObject.nodeId.toString());
//xx proxyObject.emit("execution_flag_changed",proxyObject.executableFlag);
});
};
exports.UAProxyManager = UAProxyManager;
const thenify = require("thenify");
UAProxyManager.prototype.start = thenify.withCallback(UAProxyManager.prototype.start);
UAProxyManager.prototype.stop = thenify.withCallback(UAProxyManager.prototype.stop);
UAProxyManager.prototype.getObject = thenify.withCallback(UAProxyManager.prototype.getObject);
ProxyBaseNode.prototype.readValue = thenify.withCallback(ProxyBaseNode.prototype.readValue);
ProxyBaseNode.prototype.writeValue = thenify.withCallback(ProxyBaseNode.prototype.writeValue);