From cfe76ef92ecb3e7a6d1200db45bb19ddda42d5c7 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Tue, 12 Feb 2013 18:35:30 +0100 Subject: [PATCH] JS: Handle account events through Account class. --- src/js/account.js | 73 ++++++++++++++++++++++++++++++++++++++++----- src/js/meta.js | 76 +++++++++++++++++++++++++++++++++++++++++++++++ src/js/remote.js | 32 ++++++++++++++++++++ 3 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 src/js/meta.js diff --git a/src/js/account.js b/src/js/account.js index b1264352..49e35025 100644 --- a/src/js/account.js +++ b/src/js/account.js @@ -9,15 +9,74 @@ // var network = require("./network.js"); -var EventEmitter = require('events').EventEmitter; -var Amount = require('./amount.js').Amount; -var UInt160 = require('./amount.js').UInt160; +var EventEmitter = require('events').EventEmitter; +var Amount = require('./amount').Amount; +var UInt160 = require('./uint160').UInt160; -var Account = function (network, account) { - this._network = network; - this._account = UInt160.json_rewrite(account); +var Account = function (remote, account) { + var self = this; - return this; + this._remote = remote; + this._account = UInt160.from_json(account); + + // Ledger entry object + // Important: This must never be overwritten, only extend()-ed + this._entry = {}; + + this.on('newListener', function (type, listener) { + if (Account.subscribe_events.indexOf(type) !== -1) { + if (!this._subs && 'open' === this._remote._online_state) { + this._remote.request_subscribe() + .accounts(this._account.to_json()) + .request(); + } + this._subs += 1; + } + }); + + this.on('removeListener', function (type, listener) { + if (Account.subscribe_events.indexOf(type) !== -1) { + this._subs -= 1; + + if (!this._subs && 'open' === this._remote._online_state) { + this._remote.request_unsubscribe([ 'transactions' ]) + .accounts(this._account.to_json()) + .request(); + } + } + }); + + this._remote.on('connect', function () { + if (self._subs) { + this._remote.request_subscribe() + .accounts(this._account.to_json()) + .request(); + } + }); + + return this; +}; + +Account.prototype = new EventEmitter; + +/** + * List of events that require a remote subscription to the account. + */ +Account.subscribe_events = ['transaction', 'entry']; + +Account.prototype.to_json = function () +{ + return this._account.to_json(); +}; + +/** + * Whether the AccountId is valid. + * + * Note: This does not tell you whether the account exists in the ledger. + */ +Account.prototype.is_valid = function () +{ + return this._account.is_valid(); }; exports.Account = Account; diff --git a/src/js/meta.js b/src/js/meta.js new file mode 100644 index 00000000..ac0e108c --- /dev/null +++ b/src/js/meta.js @@ -0,0 +1,76 @@ +var extend = require('extend'); + +/** + * Meta data processing facility. + */ +var Meta = function (raw_data) +{ + this.nodes = []; + + for (var i = 0, l = raw_data.AffectedNodes.length; i < l; i++) { + var an = raw_data.AffectedNodes[i], + result = {}; + + ["CreatedNode", "ModifiedNode", "DeletedNode"].forEach(function (x) { + if (an[x]) result.diffType = x; + }); + + if (!result.diffType) return null; + + an = an[result.diffType]; + + result.entryType = an.LedgerEntryType; + result.ledgerIndex = an.LedgerIndex; + + result.fields = extend({}, an.PreviousFields, an.NewFields, an.FinalFields); + result.fieldsPrev = an.PreviousFields || {}; + result.fieldsNew = an.NewFields || {}; + result.fieldsFinal = an.FinalFields || {}; + + this.nodes.push(result); + } +}; + +/** + * Execute a function on each affected node. + * + * The callback is passed two parameters. The first is a node object which looks + * like this: + * + * { + * // Type of diff, e.g. CreatedNode, ModifiedNode + * diffType: 'CreatedNode' + * + * // Type of node affected, e.g. RippleState, AccountRoot + * entryType: 'RippleState', + * + * // Index of the ledger this change occurred in + * ledgerIndex: '01AB01AB...', + * + * // Contains all fields with later versions taking precedence + * // + * // This is a shorthand for doing things like checking which account + * // this affected without having to check the diffType. + * fields: {...}, + * + * // Old fields (before the change) + * fieldsPrev: {...}, + * + * // New fields (that have been added) + * fieldsNew: {...}, + * + * // Changed fields + * fieldsFinal: {...} + * } + * + * The second parameter to the callback is the index of the node in the metadata + * (first entry is index 0). + */ +Meta.prototype.each = function (fn) +{ + for (var i = 0, l = this.nodes.length; i < l; i++) { + fn(this.nodes[i], i); + } +} + +exports.Meta = Meta; diff --git a/src/js/remote.js b/src/js/remote.js index efe050bf..20717340 100644 --- a/src/js/remote.js +++ b/src/js/remote.js @@ -20,6 +20,8 @@ var Amount = require('./amount').Amount; var Currency = require('./amount').Currency; var UInt160 = require('./amount').UInt160; var Transaction = require('./transaction').Transaction; +var Account = require('./account').Account; +var Meta = require('./meta').Meta; var utils = require('./utils'); var config = require('./config'); @@ -233,6 +235,7 @@ var Remote = function (opts, trace) { } // Cache information for accounts. + // DEPRECATED, will be removed this.accounts = { // Consider sequence numbers stable if you know you're not generating bad transactions. // Otherwise, clear it to have it automatically refreshed from the network. @@ -241,6 +244,9 @@ var Remote = function (opts, trace) { }; + // Hash map of Account objects by AccountId. + this._accounts = {}; + // List of secrets that we know about. this.secrets = { // Secrets can be set by calling set_secret(account, secret). @@ -507,6 +513,7 @@ Remote.prototype._connect_start = function () { // It is possible for messages to be dispatched after the connection is closed. Remote.prototype._connect_message = function (ws, json) { + var self = this; var message = JSON.parse(json); var unexpected = false; var request; @@ -557,6 +564,23 @@ Remote.prototype._connect_message = function (ws, json) { case 'account': // XXX If not trusted, need proof. + // Process metadata + message.mmeta = new Meta(message.meta); + + // Pass the event on to any related Account objects + message.mmeta.each(function (an) { + if (an.entryType === 'AccountRoot') { + var account = self._accounts[an.fields.Account]; + + // Only trigger the event if the account object is actually + // subscribed - this prevents some weird phantom events from + // occurring. + if (account && account._subs) { + account.emit('transaction', message); + } + } + }); + this.emit('account', message); break; @@ -994,6 +1018,14 @@ Remote.prototype.request_owner_count = function (account, current) { }); }; +Remote.prototype.account = function (accountId) { + var account = new Account(this, accountId); + + if (!account.is_valid()) return account; + + return this._accounts[account.to_json()] = account; +}; + // Return the next account sequence if possible. // <-- undefined or Sequence Remote.prototype.account_seq = function (account, advance) {