All files exchange.js

78.57% Statements 22/28
62.5% Branches 10/16
84.62% Functions 11/13
77.78% Lines 21/27
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113                1x 1x 1x 1x 1x                         1x           1x         1x             1x   1x 1x 1x                           1x 1x                               1x     1x       1x             2x 2x 1x     1x                              
import r from 'rethinkdb';
import {Topic} from './topic';
import {Queue} from './queue';
 
// Represents a message exchange which messages can be sent to and
// consumed from. Each exchange has an underlying RethinkDB table.
export class Exchange {
    constructor(name, connOpts = {}) {
        this.db = connOpts.db = connOpts.db || 'test';
        this.name = name;
        this.conn = null;
        this.table = r.table(name);
        this._asserted = false;
 
        // Bluebird's .bind ensures `this` inside our callbacks is the exchange
        this.promise = r.connect(connOpts)
        .then(conn => this.conn = conn)
        .catch(r.Error.RqlRuntimeError, (err) => {
            console.error(err.message);
            process.exit(1);
        });
    }
 
    // Returns a topic in this exchange
    topic(name) {
        return new Topic(this, name);
    }
 
    // Returns a new queue on this exchange that will filter messages by
    // the given query
    queue(filterFunc) {
        return new Queue(this, filterFunc);
    }
 
    // The full ReQL query for a given filter function
    fullQuery(filterFunc) {
        return this.table
            .changes()('new_val')
            .filter(row => filterFunc(row('topic')));
    }
 
    // Publish a message to this exchange on the given topic
    publish(topicKey, payload) {
        return this.assertTable()
        .then(() => {
            const topIsObj = Object.prototype.toString.call(topicKey) === '[object Object]';
            const topic = topIsObj ? r.literal(topicKey) : topicKey;
            return this.table
            .filter({topic})
            .update({
                payload,
                updated_on: r.now() // eslint-disable-line
            })
            .run(this.conn);
        })
        .then((updateResult) => {
            // If the topic doesn't exist yet, insert a new document. Note:
            // it's possible someone else could have inserted it in the
            // meantime and this would create a duplicate. That's a risk we
            // take here. The consequence is that duplicated messages may
            // be sent to the consumer.
            Eif (updateResult.replaced === 0) {
                return this.table
                .insert({
                    payload,
                    topic: topicKey,
                    updated_on: r.now() // eslint-disable-line
                })
                .run(this.conn);
            }
 
            return updateResult;
        });
    }
 
    // Receives a callback that is called whenever a new message comes in
    // matching the filter function
    subscribe(filterFunc, iterFunc) {
        return this.assertTable()
        .then(() => this.fullQuery(filterFunc).run(this.conn))
        .then(cursor => cursor.each((err, message) => {
            Iif (err) {
                throw err;
            }
 
            iterFunc(message.topic, message.payload);
        }));
    }
 
    // Ensures the table specified exists and has the correct primary_key
    // and durability settings
    assertTable() {
        return this.promise.then(() => {
            if (this._asserted) {
                return;
            }
 
            return r.dbCreate(this.db)
                .run(this.conn) // eslint-disable-line
                .finally(() => r.db(this.db)
                    .tableCreate(this.name)
                    .run(this.conn)
                )
                .catch(r.Error.RqlRuntimeError, (err) => {
                    if (err.msg.indexOf('already exists') === -1) {
                        throw err;
                    }
                })
                .then(() => this._asserted = true);
        });
    }
}