Initial work on the new data model.

This commit is contained in:
Julien Fontanet
2013-12-09 17:07:39 +01:00
parent 047f8a7888
commit 177f7af7d7
6 changed files with 795 additions and 148 deletions

View File

@@ -18,3 +18,8 @@ trim_trailing_whitespaces = true
[/package.json] [/package.json]
indent_size = 2 indent_size = 2
indent_style = space indent_style = space
# For CoffeeScript files, we follow this Polarmobile style guide (https://github.com/polarmobile/coffeescript-style-guide/blob/master/README.md).
[*{,.spec}.{,lit}coffee]
indent_size = 2
indent_style = space

View File

@@ -14,19 +14,22 @@
"main": "src/main.js", "main": "src/main.js",
"dependencies": { "dependencies": {
"coffee-script": "~1.6.3", "coffee-script": "~1.6.3",
"connect": ">=2.8.4", "connect": "~2.11.2",
"extendable": ">=0.0.3", "extendable": "0.0.6",
"hashy": ">=0.1.0", "hashy": "~0.1.0",
"js-yaml": ">=2.1.0", "js-yaml": "~2.1.3",
"q": ">=0.9.6", "optimist": "~0.6.0",
"sync": ">=0.2.2", "q": "~0.9.7",
"then-redis": ">=0.3.8", "sync": "~0.2.2",
"underscore": ">=1.5.2", "then-redis": "~0.3.9",
"validator": ">=1.2.1", "underscore": "~1.5.2",
"ws": ">=0.4.27", "validator": "~2.0.0",
"xmlrpc": ">=1.1.0" "ws": "~0.4.31",
"xmlrpc": "~1.1.1"
}, },
"devDependencies": { "devDependencies": {
"chai": "~1.8.1",
"mocha": "~1.14.0",
"node-inspector": "~0.6.1" "node-inspector": "~0.6.1"
}, },
"optionalDependencies": {}, "optionalDependencies": {},

374
src/MappedCollection.coffee Normal file
View File

@@ -0,0 +1,374 @@
$_ = require 'underscore'
######################################################################
class DynamicProperty
constructor: (@value, @hooks) ->
#---------------------------------------------------------------------
noop = ->
copyDeep = (value) ->
if value instanceof Array
return (copyDeep item for item in value)
if value instanceof Object
result = {}
result[key] = copyDeep item for key, item of value
return result
return value
getDeep = (obj, path) ->
return obj if path.length is 0
current = obj
i = 0
n = path.length - 1
while i < n
current = current[path[i++]]
throw new Error 'invalid path component' if current is undefined
current[path[i]]
setDeep = (obj, path, value) ->
throw new Error 'invalid path' if path.length is 0
current = obj
i = 0
n = path.length - 1
while i < n
current = current[path[i++]]
throw new Error 'invalid path component' if current is undefined
current[path[i]] = value
# @param rule Rule of the current item.
# @param item Current item.
# @param value Value of the generator item.
computeValue = (rule, item, value) ->
# @param parent The parent object of this entry (necessary for
# assignment).
# @param name The name of this entry.
# @param spec Specification for the current entry.
#
# @returns The generated value for this entry.
helper = (parent, name, spec) ->
if not $_.isObject spec
parent[name] = spec
else if spec instanceof DynamicProperty
# If there was no previous value use DynamicProperty.value,
# otherwise, just keep the previous value.
if parent[name] is undefined
# Helper is re-called for the initial value.
helper parent, name, spec.value
else if $_.isFunction spec
ctx = {rule}
ctx.__proto__ = item # Links to the current item.
parent[name] = spec.call ctx, value, item.key
else if $_.isArray spec
current = parent[name] = new Array spec.length
for entry, index in spec
helper current, index, entry
else
# It's a plain object.
current = parent[name] = {}
for key, property of spec
helper current, key, property
helper item, 'value', rule.value
######################################################################
class MappedCollection
constructor: (spec) ->
# If spec is a function, it is called with various helpers in
# `this`.
if $_.isFunction spec
ctx =
dynamic: (initialValue, hooks) ->
new DynamicProperty initialValue, hooks
noop: noop
spec = spec.call ctx
# This key function returns the identifier used to map the
# generator to the generated item.
#
# The default function uses the index if the generator collection
# is an array or the property name if it is an object.
#
# /!\: This entry MUST be overriden in rules for new items.
if spec.key?
@_key = spec.key
throw new Error 'key must be a function' unless $_.isFunction @_key
else
@_key = (_, key) -> key
spec.rules or= {}
# Rules are the core of MappedCollection, they allow to categorize
# objects and to treat them differently.
@_rules = {}
# Hooks are functions which are run when a item of a given rule
# enters, exists or is updated.
@_hooks = {}
# Initialy the collection is empty.
@_byKey = {}
# For performance concerns, items are also categorized by rules.
@_byRule = {}
# Rules are checked for conformity and created in the sytem.
for name, def of spec.rules
# If it's a function, runs it.
def = def() if $_.isFunction def
throw new Error "#{name} definition must be an object" unless $_.isObject def
# A rule can extends another (not recursive for now!).
if def.extends?
unless $_.isString def.extends
throw new Error "#{name}.extends must be a string"
if spec.rules[def.extends] is undefined
throw new Error "#{name}.extends must reference a valid rule (#{def.extends})"
$_.defaults def, spec.rules[def.extends]
rule = {name}
if def.key?
# Static rule, used to create a new item (without generator).
throw new Error "both #{name}.key and #{name}.test cannot be defined" if def.test?
# The key MUST be a string.
throw new Error "#{name}.key must be a string" unless $_.isString def.key
rule.key = if $_.isFunction def.key then def.key() else def.key
else if def.test?
# Dynamic rule, used to create new items from generator items.
# The test MUST be a function.
throw new Error "#{name}.test must be a function" unless $_.isFunction def.test
rule.test = def.test
else
# Invalid rule!
throw new Error "#{name} must have either a key or a test entry"
# A rule must have a value.
throw new Error "#{name}.value must be defined" unless def.value?
rule.value = def.value
@_rules[name] = rule
@_hooks[name] =
enter: []
update: []
exit: []
@_byRule[name] = {}
# For each rules, values are browsed and hooks are created when
# necessary (dynamic properties).
for name, rule of @_rules or {}
# Browse the value searching for dynamic properties/entries.
#
# An immediatly invoked function is used to easily handle
# recursivity.
browse = (value, path) =>
# Unless the value is an object, there is nothing to browse.
return unless $_.isObject value
# If the value is a function, it is a factory which will be
# called later, when an item will be created.
return if $_.isFunction value
# If the value is a dynamic property, grabs the initial value
# and registers its hooks.
if value instanceof DynamicProperty
hooks = value.hooks
# Browse hooks for each rules.
for name_, hooks_ of hooks
# Wraps a hook.
#
# A hook is run with a defined environment
wrap = (hook, rule, items) => # Last two vars are here to be protected from the environment.
# FIXME: @_rules[name] and @_byRule are not defined at
# this point.
rule = @_rules[name]
items = @_byRule[name]
(value, key) ->
# The current hook is runs for all items of the
# current rule.
for key, item of items
# Value of the current field.
field = getDeep item.value, path
ctx = {rule, field}
ctx.__proto__ = item # Links to the current item.
hook.call ctx, value, key
# Updates the value if it changed.
setDeep item.value, path if ctx.field isnt field
# Checks each hook is correctly defined.
{enter, update, exit} = hooks_
enter ?= update
@_hooks[name_].enter.push wrap(enter) if enter?
if not update? and exit? and enter?
update = (args...) ->
exit.apply this, args
enter.apply this, args
@_hooks[name_].update.push wrap(update) if update?
@_hooks[name_].exit.push wrap(exit) if exit?
# OPTIMIZE: do not register hooks if they are `noop`.
# FIXME: Hooks must be associated to the rule (because they
# must be run for each object of this type), and to the
# field (because it must be available through @field).
return
# If the value is an array, browse each entry.
if $_.isArray value
for entry, index in value
browse entry, path.concat(index)
return
# The value is an object, browse each property.
for key, property of value
browse property, path.concat(key)
browse rule.value, []
# If it is a static rule, creates its item right now.
if rule.key?
# Adds the item.
item = @_byKey[rule.key] = @_byRule[rule.name][rule.key] =
_ruleName: rule.name
key: rule.key
value: undefined
# Computes the value.
computeValue rule, item
# No events for static items.
get: (key) -> @_byKey[key]
getAll: ->
items = {}
for ruleName, ruleItems in @_byRule
rule = @_rules[ruleName]
# Items of private rules are not exported.
continue if rule.private
for key, {value} of ruleItems
items[key] = value
items
remove: (items) ->
itemsToRemove = {}
$_.each items, (value, key) ->
key = @_key key
itemsToRemove[key] = @_byKey[key]
@_remove items
# Adds, updates or removes items from the collections. Items not
# present are added, present are updated, and present in the
# generated collection but not in the generator are removed.
set: (items, options = {}) ->
{add, update, remove} = options
add or= true
update or= true
remove or= true
itemsToRemove = {}
if remove
$_.extend(itemsToRemove, @_byKey)
$_.each items, (value, key) =>
key = @_key key
# If the item already existed.
if @_byKey[key]
# Marks this item as not to be removed.
delete itemsToRemove[key] if remove
if update
item = @_byKey[key]
rule = @_rules[item._ruleName]
# Compute the new value.
computeValue rule, item, value
# Runs related hooks.
for hook in @_hooks[rule.name]?.update or []
hook item.value, item.key
else if add
# First we have to find to which rule this item belongs to.
rule = do =>
for _, rule of @_rules
ctx = {rule}
return rule if rule.test? and rule.test.call ctx, value, key
# If no rule has been found, just stops.
return unless rule
# Adds the item.
item = @_byKey[key] = @_byRule[rule.name][key] =
_ruleName: rule.name
key: key
value: undefined
# Computes the value.
computeValue rule, item, value
# Runs related hooks.
for hook in @_hooks[rule.name]?.enter or []
hook item.value, item.key
# There are keys inside only if remove is `true`.
@_remove itemsToRemove if remove
_remove: (items) ->
for {_ruleName: ruleName, value}, key in items
# If there are some hooks registered, runs them.
for hook in @_hooks[ruleName]?.remove or []
hook value, key
# Removes effectively the item.
delete @_byKey[key] @_byRule[ruleName][key]
######################################################################
module.exports = (spec) -> new MappedCollection spec

View File

@@ -530,39 +530,12 @@ Api.fn.server = {
// Extra methods not really bound to an object. // Extra methods not really bound to an object.
Api.fn.xo = { Api.fn.xo = {
'getStats': function () { 'getAllObjects': function () {
return this.xo.stats; return this.xo.xobjs.getAll()
}, }
'getSessionId': function (req) {
var p_pool_id = req.params.id;
if (undefined === p_pool_id)
{
throw Api.err.INVALID_PARAMS;
}
return this.xobjs.pool.first(p_pool_id).then(function (pool) {
return pool.get('sessionId');
});
},
}; };
Api.fn.xapi = { Api.fn.xapi = {
'__catchAll': function (session, req) {
var RE = /^xapi\.(.*)\.getAll$/;
var match = req.method.match(RE);
var collection;
if (!match || !(collection = this.xo.xobjs[match[1]]))
{
throw Api.err.INVALID_METHOD;
}
return collection.get();
},
'getClasses': function () {
return this.xo.xclasses;
},
'vm': { 'vm': {
'pause': function (session, req) { 'pause': function (session, req) {
@@ -576,7 +549,7 @@ Api.fn.xapi = {
var xobjs = xo.xobjs; var xobjs = xo.xobjs;
var vm; var vm;
return this.checkPermission(session, 'write').then(function () { return this.checkPermission(session, 'write').then(function () {
return xobjs.VM.first(p_id); return xobjs.get(p_id);
}).then(function (tmp) { }).then(function (tmp) {
vm = tmp; vm = tmp;
@@ -601,7 +574,7 @@ Api.fn.xapi = {
var xobjs = xo.xobjs; var xobjs = xo.xobjs;
var vm; var vm;
return this.checkPermission(session, 'write').then(function () { return this.checkPermission(session, 'write').then(function () {
return xobjs.VM.first(p_id); return xobjs.get(p_id);
}).then(function (tmp) { }).then(function (tmp) {
vm = tmp; vm = tmp;
@@ -626,7 +599,7 @@ Api.fn.xapi = {
var xobjs = xo.xobjs; var xobjs = xo.xobjs;
var vm; var vm;
return this.checkPermission(session, 'write').then(function () { return this.checkPermission(session, 'write').then(function () {
return xobjs.VM.first(p_id); return xobjs.get(p_id);
}).then(function (tmp) { }).then(function (tmp) {
vm = tmp; vm = tmp;
@@ -653,7 +626,7 @@ Api.fn.xapi = {
var xobjs = xo.xobjs; var xobjs = xo.xobjs;
var vm; var vm;
return this.checkPermission(session, 'write').then(function () { return this.checkPermission(session, 'write').then(function () {
return xobjs.VM.first(p_id); return xobjs.get(p_id);
}).then(function (tmp) { }).then(function (tmp) {
vm = tmp; vm = tmp;
@@ -682,7 +655,7 @@ Api.fn.xapi = {
var xobjs = xo.xobjs; var xobjs = xo.xobjs;
var vm; var vm;
return this.checkPermission(session, 'write').then(function () { return this.checkPermission(session, 'write').then(function () {
return xobjs.VM.first(p_id); return xobjs.get(p_id);
}).then(function (tmp) { }).then(function (tmp) {
vm = tmp; vm = tmp;
@@ -697,3 +670,30 @@ Api.fn.xapi = {
}, },
}, },
}; };
Api.fn.system = {
// Returns the list of available methods similar to XML-RPC
// introspection (http://xmlrpc-c.sourceforge.net/introspection.html).
'listMethods': function () {
var methods = [];
(function browse(container, path) {
var n = path.length;
_.each(container, function (content, key) {
path[n] = key;
if (_.isFunction(content))
{
methods.push(path.join('.'));
}
else
{
browse(content, path);
}
});
path.pop();
})(Api.fn, []);
return methods;
},
};

338
src/spec.coffee Normal file
View File

@@ -0,0 +1,338 @@
retrieveTags = (UUID) -> [] # TODO
test = (value) -> value.$type is @rule.name
remove = (array, value) ->
index = array.indexOf array, value
array.splice(index, 1) unless index is -1
####
module.exports = (refsToUUIDs) ->
get = (name, defaultValue) ->
(value) ->
if value[name] is undefined
return defaultValue
# If the value looks like an OpaqueRef, resolve it to a UUID.
value = value[name]
if refsToUUIDs[value]
refsToUUIDs[value]
else
value
->
key: (value, key) -> value.uuid or key
rules:
xo:
# The key is directly defined here because this is a new object,
# not bound to an existing item.
#
# TODO: provides a way to create multiple new items per rule.
key: '00000000-0000-0000-0000-000000000000'
# The value is an object.
value:
type: -> @rule.name
UUID: -> @key
pools: @dynamic [],
pool:
enter: (pool) -> @field.push pool.UUID
exit: (pool) -> remove @field, pool.UUID
update: @noop
$CPUs: @dynamic 0,
host:
# No `update`: `exit` then `enter` will be called instead.
enter: (host) -> @field += host.CPUs.length
exit: (host) -> @field -= host.CPUs.length
$running_VMs: @dynamic [],
VM:
# No `enter`: `update` will be called instead.
update: (VM) ->
remove @field, VM.UUID
if VM.power_state is 'Running'
@field.push VM.UUID
exit: (VM) -> remove @field, VM.UUID
$vCPUs: @dynamic 0,
VM:
# No `update`: `exit` then `enter` will be called instead.
enter: (VM) -> @field += VM.CPUs.length
exit: (VM) -> @field -= VM.CPUs.length
$memory: @dynamic { usage: 0, size: 0 },
host:
# No `update`: `exit` then `enter` will be called instead.
enter: (host) ->
@field.usage += host.memory.usage
@field.size += host.memory.size
exit: (host) ->
@field.usage -= host.memory.usage
@field.size -= host.memory.size
pool:
test: test
value:
type: -> @rule.name
UUID: -> @key
tags: -> retrieveTags @value.UUID
SRs: @dynamic [],
SR:
enter: (value) ->
if value.$pool is @value.UUID
@field.push value.UUID
exit: (value) -> remove @field, value.UUID
HA_enabled: get('ha_enabled')
hosts: @dynamic [],
host:
enter: (value) ->
if value.$pool is @value.UUID
@field.push value.UUID
exit: (value) -> remove @field, value.UUID
VMs: @dynamic [],
VM:
# FIXME: when a VM is updated, this hook will run for each
# pool even though we know which pool to update
# (`value.$pool`).
# There must be a way to fix this problem while still
# keeping a generic implementation.
#
# Note: I do not want to handle this field from the VM
# rule, it would make the maintenance harder.
update: (VM) ->
# Unless this VM belongs to this pool, there is no need
# to continue.
return unless VM.$pool is @value.UUID
# If this VM is running or paused, it is necessarily on
# a host.
if state is 'Paused' or state is 'Runnning'
remove @field, VM.UUID
return
# TODO: Check whether this VM belong to a local SR.
local = false
unless local
@field.push VM.UUID
exit: (value) -> remove @field, value.UUID
$running_hosts: @dynamic [],
host:
update: (host) ->
remove @field, host.UUID
if host.$pool is @value.UUID and host.power_state is 'Running'
@field.push host.UUID
exit: (host) -> remove @field, host.UUID
$running_VMs: @dynamic [],
VM:
update: (VM) ->
remove @field, VM.UUID
if VM.$pool is @value.UUID and VM.power_state is 'Running'
@field.push VM.UUID
exit: (VM) ->
remove @field, VM.UUID
$VMs: @dynamic (-> @value.VMs.slice 0),
VM:
update: (VM) ->
remove @field, VM.UUID
if VM.$pool is @value.UUID
@field.push VM.UUID
exit: (VM) -> remove @field, VM.UUID
host:
test: test
value:
type: -> @rule.name
UUID: -> @key
name_label: get('name_label')
name_description: get('name_description')
tags: -> retrieveTags @value.UUID
address: get('address')
controller: get('controller')
CPUs: [] # TODO
enabled: get('enabled')
hostname: get('hostname')
iSCSI_name: (value) -> value.other_config?.iscsi_iqn
memory: {} # TODO
power_state: 'Running' # TODO
SRs: [] # TODO
VMs: [] # TODO
$PBDs: [] # TODO
$pool: null # TODO
$running_VMs: [] # TODO
$vCPUs: []
VM:
test: (value) ->
value.$type is @rule.name and
not value.is_control_domain and
not value.is_a_template
value:
type: -> @rule.name
UUID: -> @key
name_label: get('name_label')
name_description: get('name_description')
tags: -> retrieveTags @value.UUID
address: -> null # TODO
memory: # TODO
usage: 0
size: 0
power_state: get('power_state')
CPUs: [] # TODO
$CPU_usage: ->
n = @value.CPUs.length
return undefined unless n
sum = 0
sum += CPU.usage for CPU in @value.CPUs
sum / n
$container: null # TODO
$VBDs: [] # TODO
'VM-controller':
extends: 'VM'
test: (value) ->
value.$type is 'VM' and value.is_control_domain
SR:
test: test
value:
type: -> @rule.name
UUID: -> @key
name_label: get('name_label')
name_description: get('name_description')
tags: -> retrieveTags @value.UUID
SR_type: get('type')
physical_usage: get('physical_utilization')
usage: get('virtual_allocation')
size: get('physical_size')
$container: null # TODO
$PBDs: [] # TODO
$VDIs: [] # TODO
PBD:
test: test
value:
type: -> @rule.name
UUID: -> @key
attached: get('currently_attached')
host: get('host')
SR: get('SR')
VDI:
test: test
value:
type: -> @rule.name
UUID: -> @key
name_label: get('name_label')
name_description: get('name_description')
# TODO: determine whether or not tags are required for a VDI.
#tags: -> retrieveTags @value.UUID
usage: get('physical_utilization')
size: get('virtual_size')
# FIXME: SR.VDIs -> VDI instead of VDI.SR -> SR.
SR: get('SR')
$VBD: null # TODO
VBD:
test: test
value:
type: -> @rule.name
UUID: -> @key
VDI: get('VDI')
VM: get('VM')

131
src/xo.js
View File

@@ -3,6 +3,7 @@ var crypto = require('crypto');
var hashy = require('hashy'); var hashy = require('hashy');
var Q = require('q'); var Q = require('q');
var createMappedCollection = require('./MappedCollection');
var MemoryCollection = require('./collection/memory'); var MemoryCollection = require('./collection/memory');
var RedisCollection = require('./collection/redis'); var RedisCollection = require('./collection/redis');
var Model = require('./model'); var Model = require('./model');
@@ -165,67 +166,10 @@ function Xo()
} }
// Connections to Xen pools/servers. // Connections to Xen pools/servers.
//this.connections = {}; this.connections = {};
// We will keep up-to-date stats in this object
this.stats = {};
} }
require('util').inherits(Xo, require('events').EventEmitter); require('util').inherits(Xo, require('events').EventEmitter);
Xo.prototype.computeStats = _.throttle(function () {
var xo = this;
var xobjs = xo.xobjs;
return Q.all([
xobjs.host.get(),
xobjs.host_metrics.get().then(function (metrics) {
return _.indexBy(metrics, 'id');
}),
xobjs.VM.get({
'is_a_template': false,
'is_control_domain': false,
}),
xobjs.VM_metrics.get().then(function (metrics) {
return _.indexBy(metrics, 'id');
}),
xobjs.SR.count(),
]).spread(function (hosts, host_metrics, vms, vms_metrics, n_srs) {
var running_vms = _.where(vms, {
'power_state': 'Running',
});
var n_cpus = 0;
var total_memory = 0;
_.each(hosts, function (host) {
n_cpus += host.host_CPUs.length;
total_memory += +host_metrics[host.metrics].memory_total;
});
var n_vifs = 0;
var n_vcpus = 0;
var used_memory = 0;
_.each(vms, function (vm) {
var metrics = vms_metrics[vm.metrics];
n_vifs += vm.VIFs.length;
n_vcpus += +metrics.VCPUs_number;
used_memory += +metrics.memory_actual;
});
xo.stats = {
'hosts': hosts.length,
'vms': vms.length,
'running_vms': running_vms.length,
'used_memory': used_memory,
'total_memory': total_memory,
'vcpus': n_vcpus,
'cpus': n_cpus,
'vifs': n_vifs,
'srs': n_srs,
};
});
}, 5000);
Xo.prototype.start = function (data) { Xo.prototype.start = function (data) {
var cfg = data.config; var cfg = data.config;
@@ -283,16 +227,8 @@ Xo.prototype.start = function (data) {
// 'VMPP', // 'VMPP',
// 'VTPM', // 'VTPM',
]; ];
xo.xobjs = {}; xo.xobjs = createMappedCollection(require('./spec'));
_.each(xo.xclasses, function (xclass) {
xo.xobjs[xclass] = new MemoryCollection();
});
// When a server is added we should connect to it and fetch data.
var xclasses_map = {}; // @todo Remove this ugly map.
_.each(xo.xclasses, function (xclass) {
xclasses_map[xclass.toLowerCase()] = xclass;
});
var connect = function (server) { var connect = function (server) {
var pool_id = server.id; var pool_id = server.id;
var xapi = new Xapi(server.host, server.username, server.password); var xapi = new Xapi(server.host, server.username, server.password);
@@ -301,53 +237,44 @@ Xo.prototype.start = function (data) {
var xclasses = xo.xclasses; var xclasses = xo.xclasses;
var xobjs = xo.xobjs; var xobjs = xo.xobjs;
// First retrieves all objects.
return Q.all(_.map(xclasses, function (xclass) { return Q.all(_.map(xclasses, function (xclass) {
var collection = xobjs[xclass]; return xapi.call(xclass +'.get_all_records')
.then(function (records) {
_.each(records, function (record) {
record.$type = xclass;
record.$pool = pool_id;
});
return xapi.call(xclass +'.get_all_records').then(function (records) { return xobjs.set(records);
records = _.map(records, function (record, ref) {
record.id = ref;
record.pool = pool_id;
record.session = xapi.sessionId;
return record;
}); });
return collection.add(records, {
'replace': true,
});
});
})).then(function () { })).then(function () {
xo.computeStats();
// Then listens for events.
return function loop() { return function loop() {
return xapi.call('event.next').then(function (events) { return xapi.call('event.next').then(function (events) {
_.each(events, function (event) { _.each(events, function (event) {
var klass = event.class; var operation = event.operation;
var collection = xobjs[xclasses_map[klass]]; var record = event.snapshot;
if (collection) var ref = event.ref;
var type = event.class;
console.log(xapi.host, operation, type, ref);
// Normalizes the model.
record.$type = type;
record.$pool = pool_id;
if ('del' === event.operation)
{ {
var operation = event.operation; xobjs.remove({ref: record});
var ref = event.ref; }
else
console.log(xapi.host, operation, klass, ref); {
xobjs.set({ref: record}, {remove: false});
if ('del' === event.operation)
{
collection.remove(event.ref);
}
else
{
var record = event.snapshot;
record.id = ref;
record.pool = pool_id;
collection.add(record, {'replace': true});
}
} }
}); });
xo.computeStats();
return loop(); return loop();
}, function (error) { }, function (error) {
if ('SESSION_NOT_REGISTERED' === error[0]) if ('SESSION_NOT_REGISTERED' === error[0])