<commit_before>'use strict';

const assert = require('assert');
const mocha = require('mocha');
const TopologicalSort = require('../');

/**
 * Validation routine for edges
 *
 * @param {TopologicalSort} sortOp
 * @param {Array<Object{from, to}>} edges
 */
function checkEdgesValid(sortOp, edges) {
    edges.forEach(edge => sortOp.addEdge(edge.from, edge.to));

    const res = sortOp.sort();
    const sortedKeys = [...res.keys()];

    edges.forEach(edge => {
        assert(
            sortedKeys.indexOf(edge.from) < sortedKeys.indexOf(edge.to),
            `Node ${edge.from} should be placed before ${edge.to} in sorted output`
        );
    });
}

describe('topological-sort', () => {
    it('should use expected API functions', () => {
        assert.strictEqual(typeof TopologicalSort, 'function', 'Main exported object should be a function');

        const sortOp = new TopologicalSort(new Map);
        assert.strictEqual(typeof sortOp.addNode, 'function', 'TopologicalSort instance should has "addNode" method');
        assert.strictEqual(typeof sortOp.addNodes, 'function', 'TopologicalSort instance should has "addNodes" method');
        assert.strictEqual(typeof sortOp.sort, 'function', 'TopologicalSort instance should has "sort" method');
        assert.strictEqual(typeof sortOp.addEdge, 'function', 'TopologicalSort instance should has "addEdge" method');
    });

    it('should throw if addNode() is invoked with already existing node', () => {
        const sortOp = new TopologicalSort(new Map);

        assert.doesNotThrow(() => {
            sortOp.addNode(1, {});
        }, 'addNode() should not throw if nodes list is empty');

        assert.throws(() => {
            sortOp.addNode(1, {});
        }, 'addNode() should throw an error if node with this key already exists');
    });

    it('should throw if addEdge() is invoked with wrong params', () => {
        const nodes = new Map([['A', 1], ['B', 2]]);
        const sortOp = new TopologicalSort(nodes);

        assert.doesNotThrow(() => {
            sortOp.addEdge('A', 'B');
        }, 'addEdge() should not throw for valid params');

        assert.throws(() => {
            sortOp.addEdge('A', 'C');
        }, 'addNode() should throw an error if any of the params are not valid node keys');

        assert.throws(() => {
            sortOp.addEdge('C', 'D');
        }, 'addNode() should throw an error if any of the params are not valid node keys');

        assert.throws(() => {
            sortOp.addEdge('A', 'C');
        }, 'addNode() should throw an error if edge already exists');
    });

    it('should throw if addNodes() is invoked with already existing node', () => {
        const nodes = new Map([['A', 1], ['B', 2]]);
        const sortOp = new TopologicalSort(nodes);

        assert.throws(() => {
            const newNodes = new Map([['B', 3]]);
            sortOp.addNodes(newNodes);
        }, 'addNodes() should throw an error if nodes already exists');
    });

    it('should return map after sort()', () => {
        const nodes = new Map([['A', 1], ['B', 2], ['C', 3]]);
        const sortOp = new TopologicalSort(nodes);
        const res = sortOp.sort();

        assert.strictEqual(res instanceof Map, true, 'TopologicalSort sort() result variable should be a Map instance');
        assert.strictEqual(res.size, 3);
    });

    it('should sort nodes passed in constructor only + addEdge()', () => {
        const nodes = new Map([
            ['A', 1],
            ['B', 2],
            ['C', 3],
            ['D', 4],
            ['E', 5],
            ['F', 6],
            ['G', 7],
            ['H', 8]
        ]);
        const sortOp = new TopologicalSort(nodes);
        const edges = [
            {from: 'A', to: 'C'},
            {from: 'B', to: 'C'},
            {from: 'B', to: 'D'},
            {from: 'C', to: 'E'},
            {from: 'D', to: 'F'},
            {from: 'E', to: 'F'},
            {from: 'E', to: 'H'},
            {from: 'F', to: 'G'}
        ];

        checkEdgesValid(sortOp, edges);
    });

    it('should sort nodes passed in constructor + addNode()', () => {
        const nodes = new Map([['variables', 'file://...']]);
        const sortOp = new TopologicalSort(nodes);

        sortOp.addNode('mixins', 'file://...');
        sortOp.addNode('block', 'file://...');
        sortOp.addNode('block_mod_val', 'file://...');

        const edges = [
            {from: 'variables', to: 'mixins'},
            {from: 'variables', to: 'block'},
            {from: 'mixins', to: 'block'},
            {from: 'block', to: 'block_mod_val'}
        ];

        checkEdgesValid(sortOp, edges);
    });

    it('should sort nodes passed in constructor + addNodes()', () => {
        const nodes = new Map([['variables', 'file://...']]);
        const sortOp = new TopologicalSort(nodes);

        sortOp.addNodes(new Map([
            ['mixins', 'file://...'],
            ['argument', 'file://...'],
            ['mixins', 'file://...'],
            ['user', 'file://...'],
            ['user-avatar', 'file://...']
        ]));

        const edges = [
            {from: 'variables', to: 'mixins'},
            {from: 'variables', to: 'argument'},
            {from: 'mixins', to: 'argument'},
            {from: 'argument', to: 'user'},
            {from: 'user-avatar', to: 'user'},
            {from: 'variables', to: 'user-avatar'},
            {from: 'mixins', to: 'user-avatar'},
            {from: 'variables', to: 'user'},
            {from: 'mixins', to: 'user'}
        ];

        checkEdgesValid(sortOp, edges);
    });

    it('should sort nodes in the same order', () => {
        const nodes = new Map([
            ['variables', 'file://...'],
            ['mixins', 'file://...'],
            ['argument', 'file://...'],
            ['mixins', 'file://...'],
            ['user', 'file://...'],
            ['user-avatar', 'file://...']
        ]);
        const sortOp = new TopologicalSort(nodes);

        const edges = [
            {from: 'variables', to: 'mixins'},
            {from: 'variables', to: 'argument'},
            {from: 'mixins', to: 'argument'},
            {from: 'argument', to: 'user'},
            {from: 'user-avatar', to: 'user'},
            {from: 'variables', to: 'user-avatar'},
            {from: 'mixins', to: 'user-avatar'},
            {from: 'variables', to: 'user'},
            {from: 'mixins', to: 'user'}
        ];

        edges.forEach(({from, to}) => sortOp.addEdge(from, to));

        const res = sortOp.sort();
        const sortedKeys = [...res.keys()];

        const res2 = sortOp.sort();
        const sortedKeys2 = [...res.keys()];

        sortedKeys.forEach((key, index) => {
            assert.strictEqual(
                sortedKeys[index],
                sortedKeys2[index],
                `Sorted nodes differ at index ${index}`
            );
        });
    });

    it('should throw if node edges form circular dependency', () => {
        const nodes = new Map([
            ['variables', 'file://...'],
            ['mixins', 'file://...'],
            ['argument', 'file://...'],
            ['mixins', 'file://...'],
            ['user', 'file://...'],
            ['user-avatar', 'file://...']
        ]);
        const sortOp = new TopologicalSort(nodes);

        const edges = [
            {from: 'mixins', to: 'variables'},
            {from: 'variables', to: 'argument'},
            {from: 'mixins', to: 'argument'},
            {from: 'argument', to: 'user'},
            {from: 'user-avatar', to: 'user'},
            {from: 'variables', to: 'user-avatar'},
            {from: 'mixins', to: 'user-avatar'},
            {from: 'variables', to: 'user'},
            {from: 'user', to: 'mixins'}
        ];

        edges.forEach(({from, to}) => sortOp.addEdge(from, to));

        assert.throws(() => {
            sortOp.sort();
        }, 'sort() should throw if there are circular dependencies');
    });

    it('should throw if node edges form circular dependency starting from index 1', () => {
        const nodes = new Map([
            ['A', 'file://...'],
            ['B', 'file://...'],
            ['C', 'file://...'],
            ['D', 'file://...']
        ]);
        const sortOp = new TopologicalSort(nodes);

        const edges = [
            {from: 'A', to: 'B'},
            {from: 'B', to: 'C'},
            {from: 'C', to: 'D'},
            {from: 'D', to: 'B'}
        ];

        edges.forEach(({from, to}) => sortOp.addEdge(from, to));

        assert.throws(() => {
            sortOp.sort();
        }, 'sort() should throw if there are circular dependencies');
    });

    it('shouldn\'t throw for missing circular dependency', () => {
        const nodes = new Map([
            ['B', 'file://...'],
            ['A', 'file://...']
        ]);
        const sortOp = new TopologicalSort(nodes);
        sortOp.addEdge('A', 'B');

        assert.doesNotThrow(() => {
            sortOp.sort();
        }, 'sort() should not throw if node without dependencies is standing at index 0');
    });

    it('shouldn\'t loose content associated with map keys after sort', () => {
        const nodes = new Map([
            ['A', 'some A contents'],
            ['B', 'some B contents']
        ]);
        const sortOp = new TopologicalSort(nodes);
        sortOp.addEdge('A', 'B');

        const sorted = sortOp.sort();
        assert.strictEqual(sorted.get('A'), 'some A contents');
        assert.strictEqual(sorted.get('B'), 'some B contents');
    });
});
<commit_msg>Add failing test for breaking order<commit_after>'use strict';

const assert = require('assert');
const mocha = require('mocha');
const TopologicalSort = require('../');

/**
 * Validation routine for edges
 *
 * @param {TopologicalSort} sortOp
 * @param {Array<Object{from, to}>} edges
 */
function checkEdgesValid(sortOp, edges) {
    edges.forEach(edge => sortOp.addEdge(edge.from, edge.to));

    const res = sortOp.sort();
    const sortedKeys = [...res.keys()];

    edges.forEach(edge => {
        assert(
            sortedKeys.indexOf(edge.from) < sortedKeys.indexOf(edge.to),
            `Node ${edge.from} should be placed before ${edge.to} in sorted output`
        );
    });
}

describe('topological-sort', () => {
    it('should use expected API functions', () => {
        assert.strictEqual(typeof TopologicalSort, 'function', 'Main exported object should be a function');

        const sortOp = new TopologicalSort(new Map);
        assert.strictEqual(typeof sortOp.addNode, 'function', 'TopologicalSort instance should has "addNode" method');
        assert.strictEqual(typeof sortOp.addNodes, 'function', 'TopologicalSort instance should has "addNodes" method');
        assert.strictEqual(typeof sortOp.sort, 'function', 'TopologicalSort instance should has "sort" method');
        assert.strictEqual(typeof sortOp.addEdge, 'function', 'TopologicalSort instance should has "addEdge" method');
    });

    it('should throw if addNode() is invoked with already existing node', () => {
        const sortOp = new TopologicalSort(new Map);

        assert.doesNotThrow(() => {
            sortOp.addNode(1, {});
        }, 'addNode() should not throw if nodes list is empty');

        assert.throws(() => {
            sortOp.addNode(1, {});
        }, 'addNode() should throw an error if node with this key already exists');
    });

    it('should throw if addEdge() is invoked with wrong params', () => {
        const nodes = new Map([['A', 1], ['B', 2]]);
        const sortOp = new TopologicalSort(nodes);

        assert.doesNotThrow(() => {
            sortOp.addEdge('A', 'B');
        }, 'addEdge() should not throw for valid params');

        assert.throws(() => {
            sortOp.addEdge('A', 'C');
        }, 'addNode() should throw an error if any of the params are not valid node keys');

        assert.throws(() => {
            sortOp.addEdge('C', 'D');
        }, 'addNode() should throw an error if any of the params are not valid node keys');

        assert.throws(() => {
            sortOp.addEdge('A', 'C');
        }, 'addNode() should throw an error if edge already exists');
    });

    it('should throw if addNodes() is invoked with already existing node', () => {
        const nodes = new Map([['A', 1], ['B', 2]]);
        const sortOp = new TopologicalSort(nodes);

        assert.throws(() => {
            const newNodes = new Map([['B', 3]]);
            sortOp.addNodes(newNodes);
        }, 'addNodes() should throw an error if nodes already exists');
    });

    it('should not change order of input nodes if no edges exist', () => {
        const nodes = new Map([['A', 1], ['B', 2], ['C', 3]]);
        const sortOp = new TopologicalSort(nodes);
        const res = sortOp.sort();
        const sortedKeys = [...res.keys()];

        assert.strictEqual(sortedKeys[0], 'A');
        assert.strictEqual(sortedKeys[1], 'B');
        assert.strictEqual(sortedKeys[2], 'C');
    });

    it('should return map after sort()', () => {
        const nodes = new Map([['A', 1], ['B', 2], ['C', 3]]);
        const sortOp = new TopologicalSort(nodes);
        const res = sortOp.sort();

        assert.strictEqual(res instanceof Map, true, 'TopologicalSort sort() result variable should be a Map instance');
        assert.strictEqual(res.size, 3);
    });

    it('should sort nodes passed in constructor only + addEdge()', () => {
        const nodes = new Map([
            ['A', 1],
            ['B', 2],
            ['C', 3],
            ['D', 4],
            ['E', 5],
            ['F', 6],
            ['G', 7],
            ['H', 8]
        ]);
        const sortOp = new TopologicalSort(nodes);
        const edges = [
            {from: 'A', to: 'C'},
            {from: 'B', to: 'C'},
            {from: 'B', to: 'D'},
            {from: 'C', to: 'E'},
            {from: 'D', to: 'F'},
            {from: 'E', to: 'F'},
            {from: 'E', to: 'H'},
            {from: 'F', to: 'G'}
        ];

        checkEdgesValid(sortOp, edges);
    });

    it('should sort nodes passed in constructor + addNode()', () => {
        const nodes = new Map([['variables', 'file://...']]);
        const sortOp = new TopologicalSort(nodes);

        sortOp.addNode('mixins', 'file://...');
        sortOp.addNode('block', 'file://...');
        sortOp.addNode('block_mod_val', 'file://...');

        const edges = [
            {from: 'variables', to: 'mixins'},
            {from: 'variables', to: 'block'},
            {from: 'mixins', to: 'block'},
            {from: 'block', to: 'block_mod_val'}
        ];

        checkEdgesValid(sortOp, edges);
    });

    it('should sort nodes passed in constructor + addNodes()', () => {
        const nodes = new Map([['variables', 'file://...']]);
        const sortOp = new TopologicalSort(nodes);

        sortOp.addNodes(new Map([
            ['mixins', 'file://...'],
            ['argument', 'file://...'],
            ['mixins', 'file://...'],
            ['user', 'file://...'],
            ['user-avatar', 'file://...']
        ]));

        const edges = [
            {from: 'variables', to: 'mixins'},
            {from: 'variables', to: 'argument'},
            {from: 'mixins', to: 'argument'},
            {from: 'argument', to: 'user'},
            {from: 'user-avatar', to: 'user'},
            {from: 'variables', to: 'user-avatar'},
            {from: 'mixins', to: 'user-avatar'},
            {from: 'variables', to: 'user'},
            {from: 'mixins', to: 'user'}
        ];

        checkEdgesValid(sortOp, edges);
    });

    it('should sort nodes in the same order', () => {
        const nodes = new Map([
            ['variables', 'file://...'],
            ['mixins', 'file://...'],
            ['argument', 'file://...'],
            ['mixins', 'file://...'],
            ['user', 'file://...'],
            ['user-avatar', 'file://...']
        ]);
        const sortOp = new TopologicalSort(nodes);

        const edges = [
            {from: 'variables', to: 'mixins'},
            {from: 'variables', to: 'argument'},
            {from: 'mixins', to: 'argument'},
            {from: 'argument', to: 'user'},
            {from: 'user-avatar', to: 'user'},
            {from: 'variables', to: 'user-avatar'},
            {from: 'mixins', to: 'user-avatar'},
            {from: 'variables', to: 'user'},
            {from: 'mixins', to: 'user'}
        ];

        edges.forEach(({from, to}) => sortOp.addEdge(from, to));

        const res = sortOp.sort();
        const sortedKeys = [...res.keys()];

        const res2 = sortOp.sort();
        const sortedKeys2 = [...res.keys()];

        sortedKeys.forEach((key, index) => {
            assert.strictEqual(
                sortedKeys[index],
                sortedKeys2[index],
                `Sorted nodes differ at index ${index}`
            );
        });
    });

    it('should throw if node edges form circular dependency', () => {
        const nodes = new Map([
            ['variables', 'file://...'],
            ['mixins', 'file://...'],
            ['argument', 'file://...'],
            ['mixins', 'file://...'],
            ['user', 'file://...'],
            ['user-avatar', 'file://...']
        ]);
        const sortOp = new TopologicalSort(nodes);

        const edges = [
            {from: 'mixins', to: 'variables'},
            {from: 'variables', to: 'argument'},
            {from: 'mixins', to: 'argument'},
            {from: 'argument', to: 'user'},
            {from: 'user-avatar', to: 'user'},
            {from: 'variables', to: 'user-avatar'},
            {from: 'mixins', to: 'user-avatar'},
            {from: 'variables', to: 'user'},
            {from: 'user', to: 'mixins'}
        ];

        edges.forEach(({from, to}) => sortOp.addEdge(from, to));

        assert.throws(() => {
            sortOp.sort();
        }, 'sort() should throw if there are circular dependencies');
    });

    it('should throw if node edges form circular dependency starting from index 1', () => {
        const nodes = new Map([
            ['A', 'file://...'],
            ['B', 'file://...'],
            ['C', 'file://...'],
            ['D', 'file://...']
        ]);
        const sortOp = new TopologicalSort(nodes);

        const edges = [
            {from: 'A', to: 'B'},
            {from: 'B', to: 'C'},
            {from: 'C', to: 'D'},
            {from: 'D', to: 'B'}
        ];

        edges.forEach(({from, to}) => sortOp.addEdge(from, to));

        assert.throws(() => {
            sortOp.sort();
        }, 'sort() should throw if there are circular dependencies');
    });

    it('shouldn\'t throw for missing circular dependency', () => {
        const nodes = new Map([
            ['B', 'file://...'],
            ['A', 'file://...']
        ]);
        const sortOp = new TopologicalSort(nodes);
        sortOp.addEdge('A', 'B');

        assert.doesNotThrow(() => {
            sortOp.sort();
        }, 'sort() should not throw if node without dependencies is standing at index 0');
    });

    it('shouldn\'t loose content associated with map keys after sort', () => {
        const nodes = new Map([
            ['A', 'some A contents'],
            ['B', 'some B contents']
        ]);
        const sortOp = new TopologicalSort(nodes);
        sortOp.addEdge('A', 'B');

        const sorted = sortOp.sort();
        assert.strictEqual(sorted.get('A'), 'some A contents');
        assert.strictEqual(sorted.get('B'), 'some B contents');
    });
});
<|endoftext|>


<commit_before>import {Promise} from 'bluebird'
import {Registry} from './registry'
import {Set} from 'immutable'
import * as u from './useful'
import {read, set, remove, push} from './firebase_useful'
import log4js from 'log4js'
import {TODO_TRX_PATH, DONE_TRX_PATH, INTERNAL_TRX_PATH} from './settings'

let logger = log4js.getLogger('transactor')

// if the logging is not configured, use densible default
// whether logging was configured or not is determined
// by log4js.configured singleton
function configureLogging() {
  if (!log4js.configured) {
    log4js.configure({
      appenders: [{
        type: 'console',
        layout: {
          type: 'pattern',
          pattern: '[%[%5.5p%]] - %m'
        }
      }]
    })
    logger.setLevel('INFO')
  }
}

class AbortError {
  constructor(msg) {
    this.msg = msg
  }
}

class UserAbort {
  constructor(msg) {
    this.msg = msg
  }
}

/***
 * ## Server
 *
 * #### transactor(firebase, handlers, options = {})
 *
 *   Starts transactor.
 *
 *     args:
 *       firebase: firebase ref
 *       handlers: {String type: transaction_handler} map
 *       options:
 *         todoTrxRef: Firebase ref, where new transactions appear in Firebase
 *         closedTrxRef: Firebase ref, where finished transactions are put
 *         internalRef: Firebase ref, Transactor put pending writes here
 *         trCountLimit: int, concurency ammount; how many transactions should be processed at once
 *         rescheduleDelay: int, how long to wait (in ms) before re-scheduling aborted transaction
 *
 *     returns: handler = {stop, trSummary (for debug & test purposes)},
 *
 * #### stop()
 *
 *   Stops transactor
 *
 * #### transaction_handler({abort, change, push, read, set, update}, data)
 *
 *   User defined function, transaction handler associated to certain transaction type.
 *
 *   First argument are custom firebase-access functions that you can use to manipulate DB, the second argument is
 *   'data' which corresponds to submitTransaction's 'data'. Author of transaction_handler must
 *   return Promise that fulfills when the transaction is finished. If the Promise got rejected, the
 *   transaction gets to the similar state as if it was aborted by user by calling 'abort'; the
 *   Promise returned by submitTransaction will fulfill with {error: message}
 *
 * Most of Firebase-accessors functions accept keypath argument. Unlike Firebase reference, this is
 * specified as a simple array of keys, i.e. ['user', 123, 'name'] may represent path to users name.
 *
 * #### read(keypath)
 *   Reads the value.
 *     returns: Promise(value_read)
 *
 * #### set(keypath, val)
 *   Sets the value. This is synchronous process; transactor just remembers the write that should
 *   happen. Returns nothing.
 *
 * #### push(keypath, val)
 *   Analogous to Firebase's push
 *
 * #### change(keypath, fn)
 *   Read the value from given location, then set this location to fn(value)
 *
 * #### update(keypath, obj)
 *   Update the location's value with all k,v pair present in object obj. Since this is just a bunch
 *   of sets, it's also synchronous operation.
 *
 * #### abort(msg)
 * Aborts the transaction. None modification done will be saved to the DB, the transaction will be
 * understood as finished and the transactor won't try to repeat it. The Promise returned by
 * submitTransaction() call (on client side) will fulfill with {userError: msg}
 * Argument 'msg' is any object serializable by Firebase (ususally the simple String)
 *
 * #### getId()
 *   Convenience function, returns unique Firebase ID.
 *
 ***/
export function transactor(firebase, handlers, options = {}) {

  const {
    todoTrxRef = firebase.child(TODO_TRX_PATH),
    closedTrxRef = firebase.child(DONE_TRX_PATH),
    internalRef = firebase.child(INTERNAL_TRX_PATH),
    internalWritesRef = internalRef.child('writes'),
    trCountLimit = 50,
    rescheduleDelay = 100
  } = options

  if (!log4js.configured) configureLogging()
  if (options.logger) {
    logger = options.logger
  }

  const runs = {} // run id mapped to trData
  const prohibited = {}
  const waiting = []
  const trSummary = {aborted: 0, tried: 0, processed: 0}
  const registry = new Registry()
  let nextTrId = 0
  let nextRunId = 0
  let trCount = 0

  function _abort(id) {
    registry.cleanup(id)
    delete runs[id]
  }

  function refFromPath(path) {
    return firebase.child(path.join('/'))
  }

  function pushWaiting(val) {
    if (val == null) {
      throw new Error('val should be trx, not null/undefined')
    }
    waiting.push(val)
    tryConsumeWaiting()
  }

  function scheduleLater(trData) {
    setTimeout(() => {pushWaiting(trData)}, rescheduleDelay)
  }

  // --abortAndReschedule
  function abortAndReschedule(id) {
    if (runs[id] == null) {
      throw new Error('shouldnt abort transaction that is already aborted')
    }
    logger.debug(`CLEANUP & ABORT : tr no ${id}`)
    trSummary['aborted'] += 1
    scheduleLater(runs[id])
    _abort(id)
  }

  // --processTr
  function processTr(id) {
    logger.debug(`starting process ${id}`)
    const {type, data} = runs[id]
    const handler = handlers[type]
    if (handler == null) {
      throw new Error(`handler for type ${type} does not exist. Full trData: ${runs[id]}`)
    }

    if (runs[id] == null) {
      throw new Error('given run does not exist!')
    }

    return Promise.resolve()
      .then(() => {
        logger.debug(`starting handler ${id}`)
        trSummary['tried'] += 1
        return handler({
          abort: userAbort,
          change: userChange,
          getId: userGetId,
          push: userPush,
          read: userRead,
          set: userSet,
          update: userUpdate,
        }, data)
      })
      .then((result) => {
        prohibited[id] = true
        setTimeout(() => {delete prohibited[id]}, 30000)
        return result
      })
      .catch((err) => {
        prohibited[id] = true
        if (!(err instanceof AbortError)) {
          if (err instanceof UserAbort) {
            logger.debug(`user abort ${id}, msg: ${err.msg}`)
            return {userError: err.msg}
          } else {
            if (!process.env.supressErrors) {
              logger.error(`Unknown error abort ${err} ${err.stack}`)
              console.error(err)
              console.error(err.stack)
            }
            return {error: `${err}`}
          }
        }
        throw err
      })
      .then((result) => {
        // even if no Error is thrown, the transaction might be aborted in the
        // very last moment. Better check for it (userabort is not relevant now, as
        // we want to process such transaction)
        if (runs[id] != null) {
          logger.debug(`FINISH: tr no ${id}`)
          let userAborted = runs[id].status === 'useraborted'
          let writes = userAborted ? [] : registry.writesByTrx.get(id, [])
          let writesRef = push(internalWritesRef, {})
          trSummary['processed'] += 1
          runs[id].status = 'finishing'
          set(writesRef, writes)
          writes.forEach((write) => {
            // TODO immutable destructuring
            set(refFromPath(write.get('path')), write.get('value'))
          })
          let trData = runs[id]
          remove(writesRef)
          try {
            // TODO handle this better (some parts of result might get lost
            // here, so warn user about it)
            result = JSON.parse(JSON.stringify({result}))
          } catch (e) {
            let msg = `Error for transaction id=${id}, result cannot be saved\n to firebase (result=${result})`
            throw new Error(msg)
          }
          set(closedTrxRef.child(trData.frbId), {data: trData, ...result})
          remove(todoTrxRef.child(trData.frbId))
          registry.cleanup(id)
          delete runs[id]
        }
      })
      .catch((err) => {
        if (err instanceof AbortError) {
          logger.debug(`abort error ${id}`)
          return
        }
        throw err
      })

    function checkAbort() {
      if (prohibited[id]) {
        logger.warn('Unchained promise; you probably forgot to chain the promises in your transaction correctly.')
      }
      if (runs[id] == null || runs[id].status === 'useraborted') {
        logger.debug('checkAbort: throwing abort')
        throw new AbortError('Transaction was aborted')
      }
    }

    function handlePossibleConflict(_conflict) {
      if (_conflict.constructor !== Set) {
        throw new Error('conflict must be of a type immutable.Set')
      }
      checkAbort()
      let conflict = _conflict.delete(id)
      let conflictTrIds = conflict.map((i) => runs[i].trId)
      let trId = runs[id].trId
      let finishingTrIds = conflict.filter((i) => runs[i].status === 'finishing').map((i) => runs[i].trId)
      if (!conflict.isEmpty()) {
        if (u.any(conflict, (i) => runs[i].status === 'finishing') ||
            conflictTrIds.min() < trId) {
          logger.debug(`aborting ${trId}, because of ${conflictTrIds}, finishing: ${finishingTrIds}`)
          abortAndReschedule(id)
        } else {
          logger.debug(`aborting ${conflictTrIds}, because of ${trId}, finishing: ${finishingTrIds}`)
          conflict.forEach(abortAndReschedule)
        }
      }
      checkAbort()
    }

    function normalizePath(path) {
      if (u.isImmutable(path)) path = path.toJS()
      if (u.isArray(path)) return path
      throw new Error(`path must be an array or immutable list; got ${path} instead`)
    }

    // TODO if possible, make DB operations accept also firebase ref
    function userRead(path) {
      path = normalizePath(path)
      if (u.any(path, (e) => (e == null))) throw new Error(`READ ERROR: undefined / null present in path: ${path}`)
      handlePossibleConflict(registry.conflictingWithRead(path))
      registry.addRead(id, path)
      return Promise.resolve()
        .then(() => read(refFromPath(path)))
        .then((val) => {
          checkAbort()
          return registry.readAsIfTrx(id, path, val)
        })
    }

    function userSet(path, value) {
      path = normalizePath(path)
      handlePossibleConflict(registry.conflictingWithWrite(path))
      registry.addWrite(id, path, value)
    }

    function userAbort(msg) {
      checkAbort()
      runs[id].abortMsg = msg
      runs[id].status = 'useraborted'
      throw new UserAbort(msg)
    }

    function userPush(path, value) {
      path = normalizePath(path)
      let ref = refFromPath(path).push()
      userSet([...path, ref.key()], value)
      return ref
    }

    function userUpdate(path, values) {
      path = normalizePath(path)
      if ((values == null) || (values.constructor !== Object)) {
        throw new Error(`The value argument in update must be a JS Object, found ${values}`)
      }
      Object.keys(values).forEach((key) => userSet([...path, key], values[key]))
    }

    function userChange(path, fn) {
      path = normalizePath(path)
      if (typeof fn !== 'function') {
        throw new Error(`fn argument must be a function, got ${fn} instead`)
      }
      return userRead(path)
        .then((val) => fn(val))
        .then((res) => userSet(path, res))
    }

    function userGetId() {
      return firebase.push().key()
    }
  } // end processTr

  function tryConsumeWaiting() {
    while (waiting.length > 0 && trCount < trCountLimit) {
      trCount += 1
      let trData = waiting.shift()
      let id = nextRunId++
      runs[id] = {...trData, status: 'inprocess', id}
      processTr(id)
        .then(() => {
          trCount -= 1
          tryConsumeWaiting()
        })
    }
  }

  const canStart = read(internalWritesRef)
  .then((writesl2) => {
    const waitForWrites = []
    writesl2 = writesl2 || {}
    u.forEachKV(writesl2, (_, writesl1) => {
      for (let write of writesl1) {
        waitForWrites.push(set(refFromPath(write.path), write.value))
      }
    })
    waitForWrites.push(set(internalWritesRef, null))
    let msg = Object.keys(writesl2).length === 0 ?
      'Clean start: no pending writes from previous session'
        :
      'Succesfully applied pending writes from the last session'
    return Promise.all(waitForWrites)
      .then(() => logger.info(msg))
  })

  // run transactor, remember the firebase-listening reference
  let onChildAdded
  canStart.then(() => {
    onChildAdded = todoTrxRef.on('child_added', (childSnapshot, prevChildKey) => {
      let trData = childSnapshot.val()
      if (trData.type == null) {
        logger.error('malformed data: ', trData)
        throw new Error('malformed trData')
      }
      trData.trId = nextTrId++
      logger.debug(`SCHEDULED: tr no ${trData.trId} data: ${JSON.stringify(trData)}`)
      trData.frbId = childSnapshot.key()
      pushWaiting(trData)
    })
  })

  // --end-transactor
  return {
    'stop': () => canStart.then((_) => todoTrxRef.off('child_added', onChildAdded)),
    'trSummary': trSummary,
  }

}

// Synchronous variant of finishing transaction code
//let toWait = []
//toWait.push(set(writesRef, writes))
//writes.forEach((write) => {
//  // TODO immutable destructuring
//  toWait.push(set(refFromPath(write.get('path')), write.get('value')))
//})
//let trData = runs[id]
//  Promise.all(toWait).then((_) => {
//  remove(writesRef)
//  set(firebase.child(DONE_TRX_PATH).child(trData.frbId), trData)
//  remove(firebase.child(TODO_TRX_PATH).child(trData.frbId))
//  registry.cleanup(id)
//  delete runs[id]
//})
<commit_msg>Fix {result: undefined} bug<commit_after>import {Promise} from 'bluebird'
import {Registry} from './registry'
import {Set} from 'immutable'
import * as u from './useful'
import {read, set, remove, push} from './firebase_useful'
import log4js from 'log4js'
import {TODO_TRX_PATH, DONE_TRX_PATH, INTERNAL_TRX_PATH} from './settings'

let logger = log4js.getLogger('transactor')

// if the logging is not configured, use densible default
// whether logging was configured or not is determined
// by log4js.configured singleton
function configureLogging() {
  if (!log4js.configured) {
    log4js.configure({
      appenders: [{
        type: 'console',
        layout: {
          type: 'pattern',
          pattern: '[%[%5.5p%]] - %m'
        }
      }]
    })
    logger.setLevel('INFO')
  }
}

class AbortError {
  constructor(msg) {
    this.msg = msg
  }
}

class UserAbort {
  constructor(msg) {
    this.msg = msg
  }
}

/***
 * ## Server
 *
 * #### transactor(firebase, handlers, options = {})
 *
 *   Starts transactor.
 *
 *     args:
 *       firebase: firebase ref
 *       handlers: {String type: transaction_handler} map
 *       options:
 *         todoTrxRef: Firebase ref, where new transactions appear in Firebase
 *         closedTrxRef: Firebase ref, where finished transactions are put
 *         internalRef: Firebase ref, Transactor put pending writes here
 *         trCountLimit: int, concurency ammount; how many transactions should be processed at once
 *         rescheduleDelay: int, how long to wait (in ms) before re-scheduling aborted transaction
 *
 *     returns: handler = {stop, trSummary (for debug & test purposes)},
 *
 * #### stop()
 *
 *   Stops transactor
 *
 * #### transaction_handler({abort, change, push, read, set, update}, data)
 *
 *   User defined function, transaction handler associated to certain transaction type.
 *
 *   First argument are custom firebase-access functions that you can use to manipulate DB, the second argument is
 *   'data' which corresponds to submitTransaction's 'data'. Author of transaction_handler must
 *   return Promise that fulfills when the transaction is finished. If the Promise got rejected, the
 *   transaction gets to the similar state as if it was aborted by user by calling 'abort'; the
 *   Promise returned by submitTransaction will fulfill with {error: message}
 *
 * Most of Firebase-accessors functions accept keypath argument. Unlike Firebase reference, this is
 * specified as a simple array of keys, i.e. ['user', 123, 'name'] may represent path to users name.
 *
 * #### read(keypath)
 *   Reads the value.
 *     returns: Promise(value_read)
 *
 * #### set(keypath, val)
 *   Sets the value. This is synchronous process; transactor just remembers the write that should
 *   happen. Returns nothing.
 *
 * #### push(keypath, val)
 *   Analogous to Firebase's push
 *
 * #### change(keypath, fn)
 *   Read the value from given location, then set this location to fn(value)
 *
 * #### update(keypath, obj)
 *   Update the location's value with all k,v pair present in object obj. Since this is just a bunch
 *   of sets, it's also synchronous operation.
 *
 * #### abort(msg)
 * Aborts the transaction. None modification done will be saved to the DB, the transaction will be
 * understood as finished and the transactor won't try to repeat it. The Promise returned by
 * submitTransaction() call (on client side) will fulfill with {userError: msg}
 * Argument 'msg' is any object serializable by Firebase (ususally the simple String)
 *
 * #### getId()
 *   Convenience function, returns unique Firebase ID.
 *
 ***/
export function transactor(firebase, handlers, options = {}) {

  const {
    todoTrxRef = firebase.child(TODO_TRX_PATH),
    closedTrxRef = firebase.child(DONE_TRX_PATH),
    internalRef = firebase.child(INTERNAL_TRX_PATH),
    internalWritesRef = internalRef.child('writes'),
    trCountLimit = 50,
    rescheduleDelay = 100
  } = options

  if (!log4js.configured) configureLogging()
  if (options.logger) {
    logger = options.logger
  }

  const runs = {} // run id mapped to trData
  const prohibited = {}
  const waiting = []
  const trSummary = {aborted: 0, tried: 0, processed: 0}
  const registry = new Registry()
  let nextTrId = 0
  let nextRunId = 0
  let trCount = 0

  function _abort(id) {
    registry.cleanup(id)
    delete runs[id]
  }

  function refFromPath(path) {
    return firebase.child(path.join('/'))
  }

  function pushWaiting(val) {
    if (val == null) {
      throw new Error('val should be trx, not null/undefined')
    }
    waiting.push(val)
    tryConsumeWaiting()
  }

  function scheduleLater(trData) {
    setTimeout(() => {pushWaiting(trData)}, rescheduleDelay)
  }

  // --abortAndReschedule
  function abortAndReschedule(id) {
    if (runs[id] == null) {
      throw new Error('shouldnt abort transaction that is already aborted')
    }
    logger.debug(`CLEANUP & ABORT : tr no ${id}`)
    trSummary['aborted'] += 1
    scheduleLater(runs[id])
    _abort(id)
  }

  // --processTr
  function processTr(id) {
    logger.debug(`starting process ${id}`)
    const {type, data} = runs[id]
    const handler = handlers[type]
    if (handler == null) {
      throw new Error(`handler for type ${type} does not exist. Full trData: ${runs[id]}`)
    }

    if (runs[id] == null) {
      throw new Error('given run does not exist!')
    }

    return Promise.resolve()
      .then(() => {
        logger.debug(`starting handler ${id}`)
        trSummary['tried'] += 1
        return handler({
          abort: userAbort,
          change: userChange,
          getId: userGetId,
          push: userPush,
          read: userRead,
          set: userSet,
          update: userUpdate,
        }, data)
      })
      .then((result) => {
        prohibited[id] = true
        setTimeout(() => {delete prohibited[id]}, 30000)
        return result
      })
      .catch((err) => {
        prohibited[id] = true
        if (!(err instanceof AbortError)) {
          if (err instanceof UserAbort) {
            logger.debug(`user abort ${id}, msg: ${err.msg}`)
            return {userError: err.msg}
          } else {
            if (!process.env.supressErrors) {
              logger.error(`Unknown error abort ${err} ${err.stack}`)
              console.error(err)
              console.error(err.stack)
            }
            return {error: `${err}`}
          }
        }
        throw err
      })
      .then((result) => {
        // even if no Error is thrown, the transaction might be aborted in the
        // very last moment. Better check for it (userabort is not relevant now, as
        // we want to process such transaction)
        if (runs[id] != null) {
          logger.debug(`FINISH: tr no ${id}`)
          let userAborted = runs[id].status === 'useraborted'
          let writes = userAborted ? [] : registry.writesByTrx.get(id, [])
          let writesRef = push(internalWritesRef, {})
          trSummary['processed'] += 1
          runs[id].status = 'finishing'
          set(writesRef, writes)
          writes.forEach((write) => {
            // TODO immutable destructuring
            set(refFromPath(write.get('path')), write.get('value'))
          })
          let trData = runs[id]
          remove(writesRef)
          // {result: undefined} cannot be put into Firebase. OTOH, {result: null} is ok.
          if (result === undefined) {
            result = null
          }
          try {
            // TODO handle this better (some parts of result might get lost
            // here, so warn user about it)
            result = JSON.parse(JSON.stringify({result}))
          } catch (e) {
            let msg = `Error for transaction id=${id}, result cannot be saved\n to firebase (result=${result})`
            throw new Error(msg)
          }
          set(closedTrxRef.child(trData.frbId), {data: trData, ...result})
          remove(todoTrxRef.child(trData.frbId))
          registry.cleanup(id)
          delete runs[id]
        }
      })
      .catch((err) => {
        if (err instanceof AbortError) {
          logger.debug(`abort error ${id}`)
          return
        }
        throw err
      })

    function checkAbort() {
      if (prohibited[id]) {
        logger.warn('Unchained promise; you probably forgot to chain the promises in your transaction correctly.')
      }
      if (runs[id] == null || runs[id].status === 'useraborted') {
        logger.debug('checkAbort: throwing abort')
        throw new AbortError('Transaction was aborted')
      }
    }

    function handlePossibleConflict(_conflict) {
      if (_conflict.constructor !== Set) {
        throw new Error('conflict must be of a type immutable.Set')
      }
      checkAbort()
      let conflict = _conflict.delete(id)
      let conflictTrIds = conflict.map((i) => runs[i].trId)
      let trId = runs[id].trId
      let finishingTrIds = conflict.filter((i) => runs[i].status === 'finishing').map((i) => runs[i].trId)
      if (!conflict.isEmpty()) {
        if (u.any(conflict, (i) => runs[i].status === 'finishing') ||
            conflictTrIds.min() < trId) {
          logger.debug(`aborting ${trId}, because of ${conflictTrIds}, finishing: ${finishingTrIds}`)
          abortAndReschedule(id)
        } else {
          logger.debug(`aborting ${conflictTrIds}, because of ${trId}, finishing: ${finishingTrIds}`)
          conflict.forEach(abortAndReschedule)
        }
      }
      checkAbort()
    }

    function normalizePath(path) {
      if (u.isImmutable(path)) path = path.toJS()
      if (u.isArray(path)) return path
      throw new Error(`path must be an array or immutable list; got ${path} instead`)
    }

    // TODO if possible, make DB operations accept also firebase ref
    function userRead(path) {
      path = normalizePath(path)
      if (u.any(path, (e) => (e == null))) throw new Error(`READ ERROR: undefined / null present in path: ${path}`)
      handlePossibleConflict(registry.conflictingWithRead(path))
      registry.addRead(id, path)
      return Promise.resolve()
        .then(() => read(refFromPath(path)))
        .then((val) => {
          checkAbort()
          return registry.readAsIfTrx(id, path, val)
        })
    }

    function userSet(path, value) {
      path = normalizePath(path)
      handlePossibleConflict(registry.conflictingWithWrite(path))
      registry.addWrite(id, path, value)
    }

    function userAbort(msg) {
      checkAbort()
      runs[id].abortMsg = msg
      runs[id].status = 'useraborted'
      throw new UserAbort(msg)
    }

    function userPush(path, value) {
      path = normalizePath(path)
      let ref = refFromPath(path).push()
      userSet([...path, ref.key()], value)
      return ref
    }

    function userUpdate(path, values) {
      path = normalizePath(path)
      if ((values == null) || (values.constructor !== Object)) {
        throw new Error(`The value argument in update must be a JS Object, found ${values}`)
      }
      Object.keys(values).forEach((key) => userSet([...path, key], values[key]))
    }

    function userChange(path, fn) {
      path = normalizePath(path)
      if (typeof fn !== 'function') {
        throw new Error(`fn argument must be a function, got ${fn} instead`)
      }
      return userRead(path)
        .then((val) => fn(val))
        .then((res) => userSet(path, res))
    }

    function userGetId() {
      return firebase.push().key()
    }
  } // end processTr

  function tryConsumeWaiting() {
    while (waiting.length > 0 && trCount < trCountLimit) {
      trCount += 1
      let trData = waiting.shift()
      let id = nextRunId++
      runs[id] = {...trData, status: 'inprocess', id}
      processTr(id)
        .then(() => {
          trCount -= 1
          tryConsumeWaiting()
        })
    }
  }

  const canStart = read(internalWritesRef)
  .then((writesl2) => {
    const waitForWrites = []
    writesl2 = writesl2 || {}
    u.forEachKV(writesl2, (_, writesl1) => {
      for (let write of writesl1) {
        waitForWrites.push(set(refFromPath(write.path), write.value))
      }
    })
    waitForWrites.push(set(internalWritesRef, null))
    let msg = Object.keys(writesl2).length === 0 ?
      'Clean start: no pending writes from previous session'
        :
      'Succesfully applied pending writes from the last session'
    return Promise.all(waitForWrites)
      .then(() => logger.info(msg))
  })

  // run transactor, remember the firebase-listening reference
  let onChildAdded
  canStart.then(() => {
    onChildAdded = todoTrxRef.on('child_added', (childSnapshot, prevChildKey) => {
      let trData = childSnapshot.val()
      if (trData.type == null) {
        logger.error('malformed data: ', trData)
        throw new Error('malformed trData')
      }
      trData.trId = nextTrId++
      logger.debug(`SCHEDULED: tr no ${trData.trId} data: ${JSON.stringify(trData)}`)
      trData.frbId = childSnapshot.key()
      pushWaiting(trData)
    })
  })

  // --end-transactor
  return {
    'stop': () => canStart.then((_) => todoTrxRef.off('child_added', onChildAdded)),
    'trSummary': trSummary,
  }

}

// Synchronous variant of finishing transaction code
//let toWait = []
//toWait.push(set(writesRef, writes))
//writes.forEach((write) => {
//  // TODO immutable destructuring
//  toWait.push(set(refFromPath(write.get('path')), write.get('value')))
//})
//let trData = runs[id]
//  Promise.all(toWait).then((_) => {
//  remove(writesRef)
//  set(firebase.child(DONE_TRX_PATH).child(trData.frbId), trData)
//  remove(firebase.child(TODO_TRX_PATH).child(trData.frbId))
//  registry.cleanup(id)
//  delete runs[id]
//})
<|endoftext|>


<commit_before>var AssetBrowser = Class.create({
  initialize: function(grouping, folder) {
    AssetBrowser.current_grouping = grouping;
    AssetBrowser.dl = $('asset-browser');
    AssetBrowser.setContentNodeForm('has-assets-form');
    AssetBrowser.dts = AssetBrowser.dl.select('dt');
    AssetBrowser.open_folder = folder;

    AssetBrowser.dts.each(function(dt) {
      dt.dd = dt.next('dd');
      if (dt.dd) {
        dt.dd.hide();
        dt.onclick = function() {
          AssetBrowser.closeInfo();
          this.dd.toggle();
          if (AssetBrowser.current_content != this.dd) {
            if (AssetBrowser.current_content != null && AssetBrowser.current_content.visible()) {
              AssetBrowser.current_content.toggle();
            }
            AssetBrowser.current_content = this.dd;
          }
        };
        new Renameable(dt, 0.7);
      }
    });
    AssetBrowser.openContent();
  }
});

Object.extend(AssetBrowser, {

  openContent: function() {
    type = this.open_folder;
    if (type) {
      dt = this.dl.down('#'+type);
      if (dt) { dt.onclick(); }
    }
  },

  openInfo: function(id, anchor) {

    this.closeInfo();
    list = $(anchor).up('ul');
    list.hide();
    list.insert({after: '

'});
    this.current_list = list;
    this.current_info = list.next();

    new Ajax.Request('/admin/assets/' + id, { method: 'get',
                                              asynchronous:true,
                                              evalScripts:true } );
  },

  closeInfo: function() {
    if (this.current_list) {
      this.current_list.show();
      this.current_info.remove();
    }
    this.current_list = null;
    this.current_info = null;
  },

  closeThumbnail: function() {
    key = Object.keys(SWFUpload.instances).last();
    SWFUpload.instances[key].destroy();
    this.closeInfo();
  },

  assetAction: function(asset) {
    $('asset-upload-form').reset();
    if (this.contentNodeForm) {
      this.addAssetToContentNode(asset);
    } else {
      this.destroyAsset(asset);
    }
  },

  destroyAsset: function(asset) {
    if (confirm('Are you sure you wish to delete the asset \'' + asset.filename + ' \'?')) {
      new Ajax.Request('/admin/assets/'+ asset.id, { method: 'delete',
                                                     asynchronous:true,
                                                     evalScripts:true,
                                                     parameters: { content_type: asset.content_type, authenticity_token: AJ.authenticity_token() } } );

      asset_li = $('browser-asset-' + asset.id);
      asset_list = asset_li.up('ul');
      asset_li.remove();
      if (asset_list.empty()) {
        asset_list.up('dd').previous().remove();
        asset_list.up('dd').remove();
      }
      if (this.contentNodeForm) {
        this.removeAssetFromContentNode(asset.id);
      }

    }
  },

  // Fire once a asset has been uploaded
  uploadedAsset: function(asset) {
    type = Object.keys(asset).first();
    $('asset-upload-form').reset();
    if (this.contentNodeForm) {
      this.addAssetToContentNode(asset[type]);
    }
    if (asset[type]) {
      switch (this.current_grouping){
        case 'by_content_type':
        this.open_folder = asset[type].content_type.replace(/\W+/,'-');
        break;
        case 'by_category':
        this.open_folder = asset[type].category;
        break;
      }
    }
  },

  reload: function(grouping) {
    if (grouping) {
      this.current_grouping = grouping;
    }
    new Ajax.Request('/admin/assets/'+this.current_grouping, { method: 'get',
                                        asynchronous:true,
                                        evalScripts:true,
                                        parameters: { for_content: this.contentNodeForm != null,
                                                      folder: this.open_folder },
                                        onSuccess: init_asset_category_auto_complete } );

  },

  setContentNodeForm: function(id) {
    this.contentNodeForm = $(id);
    if (this.contentNodeForm) {
      this.contentNodeForm.model_name = this.contentNodeForm.className.split('_').slice(1,this.contentNodeForm.className.split('_').length).join('_');
      this.setUpAssetList();
      this.contentNodeForm.addAssetIDs = function() {
        $$('input.asset_id').invoke('remove');
        assets = $$('#asset-list li');
        if (assets.size() == 0) {
            $(this).insert('<input type="hidden" name="' + this.model_name + '[asset_ids]" value="" />');
        } else {
          assets.each(function(li) {
            asset_id = li.id.split('-').last();
            this.insert('<input type="hidden" name="' + this.model_name + '[asset_ids][]" class="asset_id" value="' + asset_id + '" />');
          }, this);
        }
      };

      this.contentNodeForm.onsubmit = function() {
        this.addAssetIDs();
        return true;
      };
      this.contentNodeForm.serialize = function() {
        this.addAssetIDs();
        return Form.serialize(this);
      };
    }
  },

  setUpAssetList: function() {
    this.assetList = $('asset-list');
    if (this.assetList) {
      Sortable.create(this.assetList);
    }
    this.contentNodeForm.assetList = this.assetList;
  },

  addAssetToContentNode: function(asset) {
    if (!this.assetList) {
    $('attach-asset-list').insert({top:'
' + this.contentNodeForm.model_name + ' Files

'});
      this.setUpAssetList();
    }

    this.assetList.show();
    if (!$('asset-' + asset.id)) {

      if(/image/.test(asset.content_type)){
        li = new Element('li', { 'class': asset.content_type.replace('/','-'), id: 'asset-' + asset.id }).update('');
      } else {
        li = new Element('li', { 'class': asset.content_type.replace('/','-'), id: 'asset-' + asset.id }).update(asset.filename + ' ' + asset.description);
      }

      filename = new Element('h4').update(asset.filename);
      div = new Element('div');
      h4 = new Element('h4').update("Insert Image");
      view_info = new Element('a').update('View Info');

      view_info.onclick = function() {
        AssetBrowser.openInfo(asset.id, this);
        return false;
      };

      insert_image_large = new Element('a').update('Large | ');
      insert_image_medium = new Element('a').update('Medium | ');
      insert_image_small = new Element('a').update('Small');

      insert_image_large.onclick = function() {
        addAsset(asset.id, 'large' );
        return false;
      };
      insert_image_medium.onclick = function() {
        addAsset(asset.id, 'medium' );
        return false;
      };
      insert_image_small.onclick = function() {
        addAsset(asset.id, 'thumb' );
        return false;
      };
      insert_document = new Element('a').update(' | Insert');

      var theForm = this.contentNodeForm;

      deleter = new Element('a').update('Detach');
        deleter.onclick = function() {
        AssetBrowser.removeAssetFromContentNode(asset.id);
        return false;
      };

      div.insert(filename);
      div.insert(view_info);
      div.insert('
');
      div.insert(deleter);

      li.insert(div);

      if (/image/.test(asset.content_type)) {
        //setup for images
        div.insert(h4);
        div.insert(insert_image_large);
        div.insert(insert_image_medium);
        div.insert(insert_image_small);

      } else {
        //setup for documents
        insert_document.onclick = function() {
          addAsset(asset.id);
          return false;
        };
        div.insert(insert_document);
      }

      this.assetList.insert(li);
      this.setUpAssetList();
    }
  },

  removeAssetFromContentNode: function(asset_id) {
    $('asset-' + asset_id).remove();
  },

  rootUrl: function(url){
    if(!url)
      url = window.location.href;

    var matches = url.match(/^(?:http:\/\/)?[^\/]+/);
    if(matches.length <1){
      return url;
    }else{
      return matches[0];
    }
  },

  // For swfu only
  uploadSuccess: function(fileObj, server_data) {
    AssetBrowser.uploadedAsset(server_data.evalJSON());
  },

  uploadComplete: function(fileObj) {
    /* I want the next upload to continue automatically so I'll call startUpload here */
    if (this.getStats().files_queued > 0) {
      this.startUpload();
    } else {
      UploadProgress.finish();
      AssetBrowser.reload();
    }
  }

});

// Load SWFU
var swfu;
Event.observe(window, 'load', function() {
  if ($('asset-browser')) {
    form = $('asset-upload-form');
    form.insert({after: '

'});
    js_path =  form.action.split('?').join('.js?');
    swfu = new SWFUpload({
      // Create the custom swfupload_photos_path in the routes.rb file
      // Session name must match with environment.rb
      upload_url : js_path,
      flash_url : '/flash/swfupload.swf',
      file_post_name: 'asset[uploaded_data]',

      file_size_limit : '5 MB',
      file_upload_limit : 0,

      file_queue_error_handler : UploadHandler.fileQueueError,
      file_dialog_complete_handler : UploadHandler.fileDialogComplete,
      upload_progress_handler : UploadHandler.uploadProgress,
      upload_start_handler: UploadHandler.uploadStart,
      upload_error_handler : UploadHandler.uploadError,
      upload_success_handler : AssetBrowser.uploadSuccess,
      upload_complete_handler : AssetBrowser.uploadComplete,
      custom_settings : {
        upload_form : 'asset-upload-form'
      },

      button_placeholder_id: 'flash-button',
      button_height: 19,
      button_width: 157,
      button_image_url: '/images/admin/file-uploads.png',
      button_action : SWFUpload.BUTTON_ACTION.SELECT_FILES,
      button_disable : false,
      button_window_mode : SWFUpload.WINDOW_MODE.TRANSPARENT,

      debug: false

    });
    if (swfu) {
      form.onsubmit = function(){return false;};
      $('upload-elements').remove();
    }
  }

});

init_asset_category_auto_complete = function() {
  asset_category_field = $('asset_category');
  if (asset_category_field) {
    new Ajax.Request('/admin/assets/categories', {
      method: 'get',
      onSuccess : function(response) {
        new Autocompleter.Local('asset_category', 'asset_category_complete', response.responseJSON );
      }
    });
  }
};
document.observe('dom:loaded', init_asset_category_auto_complete);

init_edit_asset_form = function() {
  $$('form.edit_asset').first().onsubmit = function(e) {
    this.request({
      onComplete: function() {AssetBrowser.reload();}
    });

    return false;
  };
};

var Holdable = Class.create({
  initialize: function(elem, seconds, firer) {
    this.elem = elem;
    this.seconds = seconds;
    if (firer) {
      this.firer = firer;
    }

    this.elem.observe('mousedown', this.starter.bindAsEventListener(this));
    this.elem.observe('mouseup', this.clearTimeout.bindAsEventListener(this));
  },

  starter: function() {
    this.clear_id = this.firer.delay(this.seconds, this.elem);
  },

  firer: function(elem) {
    alert('NO FIRER!');
  },

  clearTimeout: function() {
    window.clearTimeout(this.clear_id);
  }
});

var Renameable = Class.create(Holdable, {
  firer: function(elem) {
    this.text_input = new Element('input', { href: 'text', value: elem.innerHTML });
    elem.update(this.text_input);
    this.text_input.focus();
    this.text_input.observe('blur', function() {
      this.replace(this.value);
    });
    this.text_input.observe('change', function() {
      asset_id = elem.id.split(':').last();
      new Ajax.Request('/admin/assets/' + asset_id + '/rename_category' , {
        parameters: { name: this.value  }
      });
    });
  }
});
<commit_msg>Fix holder folder name<commit_after>var AssetBrowser = Class.create({
  initialize: function(grouping, folder) {
    AssetBrowser.current_grouping = grouping;
    AssetBrowser.dl = $('asset-browser');
    AssetBrowser.setContentNodeForm('has-assets-form');
    AssetBrowser.dts = AssetBrowser.dl.select('dt');
    AssetBrowser.open_folder = folder;

    AssetBrowser.dts.each(function(dt) {
      dt.dd = dt.next('dd');
      if (dt.dd) {
        dt.dd.hide();
        dt.onclick = function() {
          AssetBrowser.closeInfo();
          this.dd.toggle();
          if (AssetBrowser.current_content != this.dd) {
            if (AssetBrowser.current_content != null && AssetBrowser.current_content.visible()) {
              AssetBrowser.current_content.toggle();
            }
            AssetBrowser.current_content = this.dd;
          }
        };
        new Renameable(dt, 0.7);
      }
    });
    AssetBrowser.openContent();
  }
});

Object.extend(AssetBrowser, {

  openContent: function() {
    type = this.open_folder;
    if (type) {
      dt = this.dl.down('#'+type);
      if (dt) { dt.onclick(); }
    }
  },

  openInfo: function(id, anchor) {

    this.closeInfo();
    list = $(anchor).up('ul');
    list.hide();
    list.insert({after: '

'});
    this.current_list = list;
    this.current_info = list.next();

    new Ajax.Request('/admin/assets/' + id, { method: 'get',
                                              asynchronous:true,
                                              evalScripts:true } );
  },

  closeInfo: function() {
    if (this.current_list) {
      this.current_list.show();
      this.current_info.remove();
    }
    this.current_list = null;
    this.current_info = null;
  },

  closeThumbnail: function() {
    key = Object.keys(SWFUpload.instances).last();
    SWFUpload.instances[key].destroy();
    this.closeInfo();
  },

  assetAction: function(asset) {
    $('asset-upload-form').reset();
    if (this.contentNodeForm) {
      this.addAssetToContentNode(asset);
    } else {
      this.destroyAsset(asset);
    }
  },

  destroyAsset: function(asset) {
    if (confirm('Are you sure you wish to delete the asset \'' + asset.filename + ' \'?')) {
      new Ajax.Request('/admin/assets/'+ asset.id, { method: 'delete',
                                                     asynchronous:true,
                                                     evalScripts:true,
                                                     parameters: { content_type: asset.content_type, authenticity_token: AJ.authenticity_token() } } );

      asset_li = $('browser-asset-' + asset.id);
      asset_list = asset_li.up('ul');
      asset_li.remove();
      if (asset_list.empty()) {
        asset_list.up('dd').previous().remove();
        asset_list.up('dd').remove();
      }
      if (this.contentNodeForm) {
        this.removeAssetFromContentNode(asset.id);
      }

    }
  },

  // Fire once a asset has been uploaded
  uploadedAsset: function(asset) {
    type = Object.keys(asset).first();
    $('asset-upload-form').reset();
    if (this.contentNodeForm) {
      this.addAssetToContentNode(asset[type]);
    }
    if (asset[type]) {
      switch (this.current_grouping){
        case 'by_content_type':
        this.open_folder = asset[type].content_type.replace(/\W+/,'-');
        break;
        case 'by_category':
        this.open_folder = asset[type].category;
        break;
      }
    }
  },

  reload: function(grouping) {
    if (grouping) {
      this.current_grouping = grouping;
    }
    new Ajax.Request('/admin/assets/'+this.current_grouping, { method: 'get',
                                        asynchronous:true,
                                        evalScripts:true,
                                        parameters: { for_content: this.contentNodeForm != null,
                                                      folder: this.open_folder },
                                        onSuccess: init_asset_category_auto_complete } );

  },

  setContentNodeForm: function(id) {
    this.contentNodeForm = $(id);
    if (this.contentNodeForm) {
      this.contentNodeForm.model_name = this.contentNodeForm.className.split('_').slice(1,this.contentNodeForm.className.split('_').length).join('_');
      this.setUpAssetList();
      this.contentNodeForm.addAssetIDs = function() {
        $$('input.asset_id').invoke('remove');
        assets = $$('#asset-list li');
        if (assets.size() == 0) {
            $(this).insert('<input type="hidden" name="' + this.model_name + '[asset_ids]" value="" />');
        } else {
          assets.each(function(li) {
            asset_id = li.id.split('-').last();
            this.insert('<input type="hidden" name="' + this.model_name + '[asset_ids][]" class="asset_id" value="' + asset_id + '" />');
          }, this);
        }
      };

      this.contentNodeForm.onsubmit = function() {
        this.addAssetIDs();
        return true;
      };
      this.contentNodeForm.serialize = function() {
        this.addAssetIDs();
        return Form.serialize(this);
      };
    }
  },

  setUpAssetList: function() {
    this.assetList = $('asset-list');
    if (this.assetList) {
      Sortable.create(this.assetList);
    }
    this.contentNodeForm.assetList = this.assetList;
  },

  addAssetToContentNode: function(asset) {
    if (!this.assetList) {
    $('attach-asset-list').insert({top:'
' + this.contentNodeForm.model_name + ' Files

'});
      this.setUpAssetList();
    }

    this.assetList.show();
    if (!$('asset-' + asset.id)) {

      if(/image/.test(asset.content_type)){
        li = new Element('li', { 'class': asset.content_type.replace('/','-'), id: 'asset-' + asset.id }).update('');
      } else {
        li = new Element('li', { 'class': asset.content_type.replace('/','-'), id: 'asset-' + asset.id }).update(asset.filename + ' ' + asset.description);
      }

      filename = new Element('h4').update(asset.filename);
      div = new Element('div');
      h4 = new Element('h4').update("Insert Image");
      view_info = new Element('a').update('View Info');

      view_info.onclick = function() {
        AssetBrowser.openInfo(asset.id, this);
        return false;
      };

      insert_image_large = new Element('a').update('Large | ');
      insert_image_medium = new Element('a').update('Medium | ');
      insert_image_small = new Element('a').update('Small');

      insert_image_large.onclick = function() {
        addAsset(asset.id, 'large' );
        return false;
      };
      insert_image_medium.onclick = function() {
        addAsset(asset.id, 'medium' );
        return false;
      };
      insert_image_small.onclick = function() {
        addAsset(asset.id, 'thumb' );
        return false;
      };
      insert_document = new Element('a').update(' | Insert');

      var theForm = this.contentNodeForm;

      deleter = new Element('a').update('Detach');
        deleter.onclick = function() {
        AssetBrowser.removeAssetFromContentNode(asset.id);
        return false;
      };

      div.insert(filename);
      div.insert(view_info);
      div.insert('
');
      div.insert(deleter);

      li.insert(div);

      if (/image/.test(asset.content_type)) {
        //setup for images
        div.insert(h4);
        div.insert(insert_image_large);
        div.insert(insert_image_medium);
        div.insert(insert_image_small);

      } else {
        //setup for documents
        insert_document.onclick = function() {
          addAsset(asset.id);
          return false;
        };
        div.insert(insert_document);
      }

      this.assetList.insert(li);
      this.setUpAssetList();
    }
  },

  removeAssetFromContentNode: function(asset_id) {
    $('asset-' + asset_id).remove();
  },

  rootUrl: function(url){
    if(!url)
      url = window.location.href;

    var matches = url.match(/^(?:http:\/\/)?[^\/]+/);
    if(matches.length <1){
      return url;
    }else{
      return matches[0];
    }
  },

  // For swfu only
  uploadSuccess: function(fileObj, server_data) {
    AssetBrowser.uploadedAsset(server_data.evalJSON());
  },

  uploadComplete: function(fileObj) {
    /* I want the next upload to continue automatically so I'll call startUpload here */
    if (this.getStats().files_queued > 0) {
      this.startUpload();
    } else {
      UploadProgress.finish();
      AssetBrowser.reload();
    }
  }

});

// Load SWFU
var swfu;
Event.observe(window, 'load', function() {
  if ($('asset-browser')) {
    form = $('asset-upload-form');
    form.insert({after: '

'});
    js_path =  form.action.split('?').join('.js?');
    swfu = new SWFUpload({
      // Create the custom swfupload_photos_path in the routes.rb file
      // Session name must match with environment.rb
      upload_url : js_path,
      flash_url : '/flash/swfupload.swf',
      file_post_name: 'asset[uploaded_data]',

      file_size_limit : '5 MB',
      file_upload_limit : 0,

      file_queue_error_handler : UploadHandler.fileQueueError,
      file_dialog_complete_handler : UploadHandler.fileDialogComplete,
      upload_progress_handler : UploadHandler.uploadProgress,
      upload_start_handler: UploadHandler.uploadStart,
      upload_error_handler : UploadHandler.uploadError,
      upload_success_handler : AssetBrowser.uploadSuccess,
      upload_complete_handler : AssetBrowser.uploadComplete,
      custom_settings : {
        upload_form : 'asset-upload-form'
      },

      button_placeholder_id: 'flash-button',
      button_height: 19,
      button_width: 157,
      button_image_url: '/images/admin/file-uploads.png',
      button_action : SWFUpload.BUTTON_ACTION.SELECT_FILES,
      button_disable : false,
      button_window_mode : SWFUpload.WINDOW_MODE.TRANSPARENT,

      debug: false

    });
    if (swfu) {
      form.onsubmit = function(){return false;};
      $('upload-elements').remove();
    }
  }

});

init_asset_category_auto_complete = function() {
  asset_category_field = $('asset_category');
  if (asset_category_field) {
    new Ajax.Request('/admin/assets/categories', {
      method: 'get',
      onSuccess : function(response) {
        new Autocompleter.Local('asset_category', 'asset_category_complete', response.responseJSON );
      }
    });
  }
};
document.observe('dom:loaded', init_asset_category_auto_complete);

init_edit_asset_form = function() {
  $$('form.edit_asset').first().onsubmit = function(e) {
    this.request({
      onComplete: function() {AssetBrowser.reload();}
    });

    return false;
  };
};

var Holdable = Class.create({
  initialize: function(elem, seconds, firer) {
    this.elem = elem;
    this.seconds = seconds;
    if (firer) {
      this.firer = firer;
    }

    this.elem.observe('mousedown', this.starter.bindAsEventListener(this));
    this.elem.observe('mouseup', this.clearTimeout.bindAsEventListener(this));
  },

  starter: function() {
    this.clear_id = this.firer.delay(this.seconds, this.elem);
  },

  firer: function(elem) {
    alert('NO FIRER!');
  },

  clearTimeout: function() {
    window.clearTimeout(this.clear_id);
  }
});

var Renameable = Class.create(Holdable, {
  firer: function(elem) {
    this.text_input = new Element('input', { href: 'text', value: elem.innerHTML });
    elem.update(this.text_input);
    this.text_input.focus();
    this.text_input.observe('blur', function() {
      this.replace(this.value);
    });
    this.text_input.observe('change', function() {
      asset_id = elem.id.split('-').last();
      new Ajax.Request('/admin/assets/' + asset_id + '/rename_category' , {
        parameters: { name: this.value  }
      });
    });
  }
});
<|endoftext|>


<commit_before>/* Transcript
 *
 */

var Transcript = (function($) {

	var DEBUG = true;

	function Transcript(options) {

		this.options = {
			target: '#transcript', // The selector of element where the transcript is written to.
			src: '', // The source URL of the transcript.
			group: 'p', // Element type used to group paragraphs.
			element: 'a', // Element type used per word.
			attribute: 'm', // Attribute name that holds the timing information.
			unit: 0.001 // Milliseconds.
		};

		for(var i in options) {
			if(options.hasOwnProperty(i)) {
				this.options[i] = options[i];
			}
		}

		if(options.src) {
			this.load();
		}
	}

	Transcript.prototype = {
		load: function(src) {
			var self = this,
				$target = $(this.options.target);
			if(src) {
				this.options.src = src;
			}
			if($target.length) {
				$target.empty().load(this.options.src, function(response, status, xhr) {
					if(status === 'error') {
						self.error(xhr.status + ' ' + xhr.statusText);
					} else {
						// Sweet
					}
				});
			} else {
				this.error('target not found');
			}
		},
		parse: function() {
			//
		},
		save: function() {
			// Doubt we save transcripts from here.
		},
		error: function(msg) {
			console.log('Transcript Error: ' + msg);
		}
	};

	return Transcript;
}(jQuery));
<commit_msg>Formalized the module structure.<commit_after>/* Transcript
 *
 */

var Transcript = (function($) {

	var DEBUG = true;

	function Transcript(options) {
		var self = this;

		this.options = $.extend({
			target: '#transcript', // The selector of element where the transcript is written to.
			src: '', // The source URL of the transcript.
			group: 'p', // Element type used to group paragraphs.
			element: 'a', // Element type used per word.
			attribute: 'm', // Attribute name that holds the timing information.
			unit: 0.001, // Milliseconds.
			debug: false
		}, options);

		this.event = {
			error: 'ha:error'
		};

		if(DEBUG) {
			this._debug();
		}

		if(options.src) {
			this.load();
		}
	}

	Transcript.prototype = {
		load: function(src) {
			var self = this,
				$target = $(this.options.target);
			if(src) {
				this.options.src = src;
			}
			if($target.length) {
				$target.empty().load(this.options.src, function(response, status, xhr) {
					if(status === 'error') {
						self._error(xhr.status + ' ' + xhr.statusText + ' : ' + self.options.src);
					} else {
						// Sweet
					}
				});
			} else {
				this._error('target not found : ' + this.options.target);
			}
		},
		parse: function() {
			//
		},

		// Plan to put these into a common object and then simple inheritence by adding to prototype.
		_trigger: function(eventType, eventData) {
			var eventObject = $.extend({options: this.options}, eventData),
				event = $.Event(eventType, {ha: eventObject});
			$(this).trigger(event);
		},
		_error: function(msg) {
			var data = {msg: 'Transcript Error : ' + msg};
			this._trigger(this.event.error, data);
		},
		_debug: function() {
			var self = this;
			$.each(this.event, function(eventName, eventType) {
				$(self).on(eventType, function(event) {
					console.log(eventType + ' : ' + event.ha.msg);
				});
			});
		}
	};

	return Transcript;
}(jQuery));
<|endoftext|>


<commit_before>import CSL from "citeproc"

import {citeprocSys} from "./citeproc_sys"

/*
* Use CSL and bibDB to format all citations for the given prosemirror json citation nodes
*/
export class FormatCitations {
    constructor(allCitationInfos, citationStyle, bibDB, citationStyles, citationLocales) {
        this.allCitationInfos = allCitationInfos
        this.citationStyle = citationStyle
        this.bibDB = bibDB
        this.citationStyles = citationStyles
        this.citationLocales = citationLocales
        this.citationStyleDef = false
    }

    init() {
        this.bibliography = false
        this.citations = []
        this.bibFormats = []
        this.citationTexts = []
        this.citationType = ''
        this.formatAllCitations()
        return this.getFormattedCitations()
    }

    formatAllCitations() {
        this.allCitationInfos.forEach(cInfo => {
            this.bibFormats.push(cInfo.format)
            this.citations.push({
                citationItems: cInfo.references,
                properties: {
                    noteIndex: this.bibFormats.length
                }
            })
        })
    }

    get bibHTML() {
        // HTML
        let html = '', bib = this.bibliography
        html += '

'
        // Add entries to bibliography
        html += bib[0].bibstart + bib[1].join('') + bib[0].bibend
        return html
    }

        // CSS
    get bibCSS()  {
        let css = '\n', bibInfo = this.bibliography[0]
            css += `.csl-entry {padding-bottom: ${bibInfo.entryspacing+1}em;}\n`
            css += `.csl-bib-body {line-height: ${bibInfo.linespacing};}\n`
            if (bibInfo.hangingindent) {
                css += `
                    .csl-entry {
                        text-indent: -0.5in;
                        padding-left: 0.5in;
                    }\n`
            } else if(bibInfo["second-field-align"] === 'margin') {
                css += `
                    .csl-left-margin {
                        text-indent: -${bibInfo.maxoffset}ch;
                        width: ${bibInfo.maxoffset}ch;
                    }
                `
            } else if(bibInfo["second-field-align"] === 'flush') {
                css += `
                    .csl-left-margin {
                        width: ${bibInfo.maxoffset}ch;
                    }
                `
            }
        return css
    }

    reloadCitations(missingItems) {
        // Not all citations could be found in the database.
        // Reload the database, but don't cycle if no new matches are found.
        return this.bibDB.getDB().then(() => {
            if (missingItems.some(item => this.bibDB.db.hasOwnProperty(item))) {
                return this.init()
            } else {
                return Promise.resolve()
            }
        })
    }

    getFormattedCitations() {

        this.citationStyleDef = this.citationStyles.find(style => style.short_title === this.citationStyle)
        if (!this.citationStyleDef && this.citationStyles.length) {
            this.citationStyleDef = this.citationStyles[0]
        }
        let citeprocConnector = new citeprocSys(this.bibDB, this.citationLocales)
        let citeprocInstance = new CSL.Engine(
            citeprocConnector,
            this.citationStyleDef.contents
        )
        let allIds = []
        this.citations.forEach(cit =>
            cit.citationItems.forEach(item => allIds.push('' + item.id))
        )
        citeprocInstance.updateItems(allIds)

        let inText = citeprocInstance.cslXml.dataObj.attrs.class === 'in-text'
        let len = this.citations.length
        for (let i = 0; i < len; i++) {
            let citation = this.citations[i],
                citationText = citeprocInstance.appendCitationCluster(citation, true)
            if (inText && 'textcite' == this.bibFormats[i]) {
                let newCiteText = '',
                    items = citation.citationItems,
                    len2 = items.length

                for (let j = 0; j < len2; j++) {
                    let onlyNameOption = [{
                        id: items[j].id,
                        "author-only": 1
                    }]

                    let onlyDateOption = [{
                        id: items[j].id,
                        "suppress-author": 1
                    }]

                    if (items[j].locator) {
                        onlyDateOption[0].locator = items[j].locator
                    }

                    if (items[j].prefix) {
                        onlyDateOption[0].prefix = items[j].prefix
                    }

                    if (0 < j) {
                        newCiteText += '; '
                    }
                    newCiteText += `${citeprocInstance.makeCitationCluster(onlyNameOption)} ${citeprocInstance.makeCitationCluster(onlyDateOption)}`
                }
                citationText[0][1] = newCiteText
            }
            this.citationTexts.push(citationText)
        }
        this.citationType = citeprocInstance.cslXml.dataObj.attrs.class
        this.bibliography = citeprocInstance.makeBibliography()

        if (citeprocConnector.missingItems.length > 0) {
            return this.reloadCitations(citeprocConnector.missingItems)
        } else {
            return Promise.resolve()
        }

    }
}
<commit_msg>don't reload bibDb when it cannot be reloaded<commit_after>import CSL from "citeproc"

import {citeprocSys} from "./citeproc_sys"

/*
* Use CSL and bibDB to format all citations for the given prosemirror json citation nodes
*/
export class FormatCitations {
    constructor(allCitationInfos, citationStyle, bibDB, citationStyles, citationLocales) {
        this.allCitationInfos = allCitationInfos
        this.citationStyle = citationStyle
        this.bibDB = bibDB
        this.citationStyles = citationStyles
        this.citationLocales = citationLocales
        this.citationStyleDef = false
    }

    init() {
        this.bibliography = false
        this.citations = []
        this.bibFormats = []
        this.citationTexts = []
        this.citationType = ''
        this.formatAllCitations()
        return this.getFormattedCitations()
    }

    formatAllCitations() {
        this.allCitationInfos.forEach(cInfo => {
            this.bibFormats.push(cInfo.format)
            this.citations.push({
                citationItems: cInfo.references,
                properties: {
                    noteIndex: this.bibFormats.length
                }
            })
        })
    }

    get bibHTML() {
        // HTML
        let html = '', bib = this.bibliography
        html += '

'
        // Add entries to bibliography
        html += bib[0].bibstart + bib[1].join('') + bib[0].bibend
        return html
    }

        // CSS
    get bibCSS()  {
        let css = '\n', bibInfo = this.bibliography[0]
            css += `.csl-entry {padding-bottom: ${bibInfo.entryspacing+1}em;}\n`
            css += `.csl-bib-body {line-height: ${bibInfo.linespacing};}\n`
            if (bibInfo.hangingindent) {
                css += `
                    .csl-entry {
                        text-indent: -0.5in;
                        padding-left: 0.5in;
                    }\n`
            } else if(bibInfo["second-field-align"] === 'margin') {
                css += `
                    .csl-left-margin {
                        text-indent: -${bibInfo.maxoffset}ch;
                        width: ${bibInfo.maxoffset}ch;
                    }
                `
            } else if(bibInfo["second-field-align"] === 'flush') {
                css += `
                    .csl-left-margin {
                        width: ${bibInfo.maxoffset}ch;
                    }
                `
            }
        return css
    }

    reloadCitations(missingItems) {
        // Not all citations could be found in the database.
        // Reload the database if possible, but don't cycle if no new matches are found.
        if (!this.bibDB.getDB) {
            return Promise.resolve()
        }

        return this.bibDB.getDB().then(() => {
            if (missingItems.some(item => this.bibDB.db.hasOwnProperty(item))) {
                return this.init()
            } else {
                return Promise.resolve()
            }
        })
    }

    getFormattedCitations() {

        this.citationStyleDef = this.citationStyles.find(style => style.short_title === this.citationStyle)
        if (!this.citationStyleDef && this.citationStyles.length) {
            this.citationStyleDef = this.citationStyles[0]
        }
        let citeprocConnector = new citeprocSys(this.bibDB, this.citationLocales)
        let citeprocInstance = new CSL.Engine(
            citeprocConnector,
            this.citationStyleDef.contents
        )
        let allIds = []
        this.citations.forEach(cit =>
            cit.citationItems.forEach(item => allIds.push('' + item.id))
        )
        citeprocInstance.updateItems(allIds)

        let inText = citeprocInstance.cslXml.dataObj.attrs.class === 'in-text'
        let len = this.citations.length
        for (let i = 0; i < len; i++) {
            let citation = this.citations[i],
                citationText = citeprocInstance.appendCitationCluster(citation, true)
            if (inText && 'textcite' == this.bibFormats[i]) {
                let newCiteText = '',
                    items = citation.citationItems,
                    len2 = items.length

                for (let j = 0; j < len2; j++) {
                    let onlyNameOption = [{
                        id: items[j].id,
                        "author-only": 1
                    }]

                    let onlyDateOption = [{
                        id: items[j].id,
                        "suppress-author": 1
                    }]

                    if (items[j].locator) {
                        onlyDateOption[0].locator = items[j].locator
                    }

                    if (items[j].prefix) {
                        onlyDateOption[0].prefix = items[j].prefix
                    }

                    if (0 < j) {
                        newCiteText += '; '
                    }
                    newCiteText += `${citeprocInstance.makeCitationCluster(onlyNameOption)} ${citeprocInstance.makeCitationCluster(onlyDateOption)}`
                }
                citationText[0][1] = newCiteText
            }
            this.citationTexts.push(citationText)
        }
        this.citationType = citeprocInstance.cslXml.dataObj.attrs.class
        this.bibliography = citeprocInstance.makeBibliography()

        if (citeprocConnector.missingItems.length > 0) {
            return this.reloadCitations(citeprocConnector.missingItems)
        } else {
            return Promise.resolve()
        }

    }
}
<|endoftext|>


<commit_before>describe('a better setTimeout', function(){
  var NOOP = function(){};
  // probably have to test using setTimeout.noConflict()
  var betterST;

  describe('setTimeout.noConflict()', function(){
      it('is used as an indicator of a better setTimeout', function(){
        expect(typeof setTimeout.noConflict).toBe('function');
      });

      it('can be called to restore the native setTimeout', function(){
        betterST = setTimeout.noConflict();
        expect(typeof setTimeout.noConflict).toBe('undefined');
      });
  });

  describe('backwards compatibility of a better setTimeout', function(){
      //
      it('is a function just like the native setTimeout()', function(){
        expect(typeof betterST).toBe('function');
      });
      it('returns a handle that can be used in the native clearTimeout()', function(){

      })
  });

  describe('setTimeout handle.clear()', function(){
    beforeEach(function(done) {
        this.handle = betterST(NOOP, 0);
        done();
    });
    it('setTimeout handle has clear method', function(){
      expect(typeof this.handle.clear).toBe('function');
      console.log(this.handle);
    })
  });

  describe('setTimeout handle.remove()', function(){
    // This is for dojo this.own() compatibility
    beforeEach(function(done) {
        this.handle = betterST(NOOP, 0);
        done();
    });
    it('handle has remove method', function(){
      expect(typeof this.handle.remove).toBe('function');
    });
    it('handle.remove is the same as handle.clear', function(){
      expect(this.handle.remove).toBe(this.handle.clear);
    })
  });

  describe('setTimeout handle.called()', function(){
    beforeEach(function(done) {
        this.handle = betterST(NOOP, 0);
        this.now = new Date().getTime();
        this.handle = betterST(NOOP,0);
        done();
    });
    it('should return a number', function(){
      expect(typeof this.handle.called()).toBe('number');
    });
    it('should be close to the present time', function(){
      var diff = this.handle.called() - this.now;
      expect(diff).toBeGreaterThan(-1);
      expect(diff).toBeLessThan(2);
    });
  });

  describe('setTimeout handle.type()', function(){
    beforeEach(function(done) {
        this.handle = betterST(NOOP, 0);
        done();
    });
    it('has type method', function(){
      expect(typeof this.handle.type).toBe('function');
    });

    it('returns "timeout" string', function(){
      expect(this.handle.type()).toBe('timeout');
    });
  });

  describe('setTimeout signature change with two new optional arguments', function(){

  });

  describe('setTimeout handle.tag()', function(){

  });

  describe('setTimeout handle.firstInvocation()', function(){
    beforeEach(function(done) {
        this.startTime = new Date().getTime();
        this.handle = betterST(function() {
            done();
        }, 500);
    });

    it('should return when the function was first invoked', function() {
        expect(this.handle.firstInvocation()).toBeGreaterThan(this.startTime);
    });
  });

  describe('setTimeout handle.interval()', function(){
    beforeEach(function(done) {
        this.handle = betterST(NOOP, 15);
        done();
    });
    it('should be the same as the argument', function(){
      expect(this.handle.interval()).toBe(15);
    })
  });

  describe('setTimeout handle.attempted()', function(){

  });

  describe('setTimeout handle.completed()', function(){

  });

  describe('a better setTimeout handle.errors()', function(){

  });

});
<commit_msg>added test for completed method<commit_after>describe('a better setTimeout', function(){
  var NOOP = function(){};
  // probably have to test using setTimeout.noConflict()
  var betterST;

  describe('setTimeout.noConflict()', function(){
      it('is used as an indicator of a better setTimeout', function(){
        expect(typeof setTimeout.noConflict).toBe('function');
      });

      it('can be called to restore the native setTimeout', function(){
        betterST = setTimeout.noConflict();
        expect(typeof setTimeout.noConflict).toBe('undefined');
      });
  });

  describe('backwards compatibility of a better setTimeout', function(){
      //
      it('is a function just like the native setTimeout()', function(){
        expect(typeof betterST).toBe('function');
      });
      it('returns a handle that can be used in the native clearTimeout()', function(){

      })
  });

  describe('setTimeout handle.clear()', function(){
    beforeEach(function(done) {
        this.handle = betterST(NOOP, 0);
        done();
    });
    it('setTimeout handle has clear method', function(){
      expect(typeof this.handle.clear).toBe('function');
      console.log(this.handle);
    })
  });

  describe('setTimeout handle.remove()', function(){
    // This is for dojo this.own() compatibility
    beforeEach(function(done) {
        this.handle = betterST(NOOP, 0);
        done();
    });
    it('handle has remove method', function(){
      expect(typeof this.handle.remove).toBe('function');
    });
    it('handle.remove is the same as handle.clear', function(){
      expect(this.handle.remove).toBe(this.handle.clear);
    })
  });

  describe('setTimeout handle.called()', function(){
    beforeEach(function(done) {
        this.handle = betterST(NOOP, 0);
        this.now = new Date().getTime();
        this.handle = betterST(NOOP,0);
        done();
    });
    it('should return a number', function(){
      expect(typeof this.handle.called()).toBe('number');
    });
    it('should be close to the present time', function(){
      var diff = this.handle.called() - this.now;
      expect(diff).toBeGreaterThan(-1);
      expect(diff).toBeLessThan(2);
    });
  });

  describe('setTimeout handle.type()', function(){
    beforeEach(function(done) {
        this.handle = betterST(NOOP, 0);
        done();
    });
    it('has type method', function(){
      expect(typeof this.handle.type).toBe('function');
    });

    it('returns "timeout" string', function(){
      expect(this.handle.type()).toBe('timeout');
    });
  });

  describe('setTimeout signature change with two new optional arguments', function(){

  });

  describe('setTimeout handle.tag()', function(){

  });

  describe('setTimeout handle.firstInvocation()', function(){
    beforeEach(function(done) {
        this.startTime = new Date().getTime();
        this.handle = betterST(function() {
            done();
        }, 500);
    });

    it('should return when the function was first invoked', function() {
        expect(this.handle.firstInvocation()).toBeGreaterThan(this.startTime);
    });
  });

  describe('setTimeout handle.interval()', function(){
    beforeEach(function(done) {
        this.handle = betterST(NOOP, 15);
        done();
    });
    it('should be the same as the argument', function(){
      expect(this.handle.interval()).toBe(15);
    })
  });

  describe('setTimeout handle.attempted()', function(){

  });

  describe('setTimeout handle.completed()', function(){
    beforeEach(function(done) {
        this.handle = betterST(function() {
            // Wait till the next turn in the JS event loop.
            setTimeout(function() { done(); }, 0);
        }, 0);
    });

    it('is a function that returns the number of times the handler has run', function() {
        expect(typeof this.handle.completed).toBe('function');
    });

    it('and should increment immediately after the handler runs', function() {
        expect(this.handle.completed()).toBe(1);
    });
  });

  describe('a better setTimeout handle.errors()', function(){

  });

});
<|endoftext|>