<commit_before>(function () {
    "use strict";

    var argv, express, corser, mongo, url, binaryBodyParser, app;

    argv = require("optimist")
             .options("port", {
                 default: 1337,
                 describe: "Port"
             })
             .options("connection-string", {
                 default: "mongodb://localhost/ming",
                 describe: "MongoDB Connection String for the default deployment."
             })
             .options("enable-proxying", {
                 default: false,
                 describe: "Allow connections to other MongoDB deployments."
             })
             .argv;
    express = require("express");
    corser = require("corser");
    mongo = require("mongodb");
    url = require("url");

    binaryBodyParser = function (req, res, next) {
        var body;
        body = new Buffer(0);
        req.on("data", function (chunk) {
            var buffer;
            buffer = new Buffer(body.length + chunk.length);
            body.copy(buffer);
            chunk.copy(buffer, body.length);
            body = buffer;
        });
        req.on("end", function () {
            req.body = body;
            next();
        });
    };

    app = express();

 // Handle CORS.
    app.use(corser.create({
        methods: corser.simpleMethods.concat(["DELETE", "PATCH"]),
        requestHeaders: corser.simpleRequestHeaders.concat(["Authorization", "X-Connection-String"]),
        responseHeaders: corser.simpleResponseHeaders.concat(["Location"])
    }));

 // Terminate CORS preflights.
    app.use(function (req, res, next) {
        if (req.method === "OPTIONS") {
            res.writeHead(204);
            res.end();
        } else {
            next();
        }
    });

 // Prepare MongoDB client.
    app.use(function (req, res, next) {
        var connectionString;
        if (argv["enable-proxying"] === true && req.headers.hasOwnProperty("x-connection-string") === true) {
            connectionString = req.headers["x-connection-string"];
        } else {
            connectionString = argv["connection-string"];
        }
        req.connectionString = url.parse(connectionString);
        mongo.MongoClient.connect(connectionString, function (err, db) {
            if (err !== null) {
                next(err);
            } else {
                req.db = db;
                next();
            }
        });
    });

 // Basic auth.
    app.use(function (req, res, next) {
        var basicAuth;
        basicAuth = express.basicAuth(function (username, password, callback) {
            req.db.authenticate(username, password, function (err) {
                if (err !== null) {
                 // Close database if authentication fails.
                    req.db.close();
                    callback(err, null);
                } else {
                    callback(null, username);
                }
            });
        }, "MongoDB");
        basicAuth(req, res, next);
    });

    app.get("/", function (req, res) {
        req.db.collectionNames({
            namesOnly: true
        }, function (err, collections) {
            var names;
            if (err !== null) {
                next(err);
            } else {
                names = collections.map(function (collection) {
                 // Strip database name.
                    return collection.substring(req.connectionString.path.length);
                });
                res.send({
                    collections: names
                });
                req.db.close();
            }
        });
    });

    app.get("/:collection", function (req, res) {
        var collectionParam;
        collectionParam = req.params.collection;
        req.db.collection(collectionParam, function (err, collection) {
            if (err !== null) {
                next(err);
            } else {
                collection.find().count(function (err, count) {
                    if (err !== null) {
                        next(err);
                    } else {
                        res.send({count: count});
                        req.db.close();
                    }
                });
            }
        });
    });

    app.get("/:prefix.files/:file", function (req, res, next) {
        var prefixParam, fileParam;
        prefixParam = req.params.prefix;
        fileParam = req.params.file;
        req.db.collection(prefixParam + ".files", function (err, collection) {
            var id;
            if (err !== null) {
                next(err);
            } else {
                try {
                    id = new mongo.ObjectID(fileParam);
                 // Get metadata first.
                    collection.findOne({
                        _id: id
                    }, function (err, document) {
                        var grid;
                        if (err !== null) {
                            next(err);
                        } else {
                            if (document === null) {
                             // Route to 404 handler.
                                next();
                            } else {
                                if (req.query.hasOwnProperty("binary") === true && req.query.binary === "true") {
                                    grid = new mongo.Grid(req.db, prefixParam);
                                    grid.get(id, function (err, file) {
                                        var contentType;
                                        if (err !== null) {
                                            next(err);
                                        } else {
                                            if (document.contentType) {
                                                contentType = document.contentType;
                                            } else if (document.filename) {
                                             // Guess file type from file extension.
                                                contentType = document.filename.match(/(\.\w+)$/)[1];
                                            } else {
                                                contentType = "application/octet-stream";
                                            }
                                            res.type(contentType);
                                            res.send(file);
                                            req.db.close();
                                        }
                                    });
                                } else {
                                    res.send(document);
                                    req.db.close();
                                }
                            }
                        }
                    });
                } catch (e) {
                 // Route to 404 handler.
                    next();
                }
            }
        });
    });

    app.get("/:collection/:document", function (req, res, next) {
        var collectionParam, documentParam;
        collectionParam = req.params.collection;
        documentParam = req.params.document;
        req.db.collection(collectionParam, function (err, collection) {
            var id;
            if (err !== null) {
                next(err);
            } else {
                try {
                    id = new mongo.ObjectID(documentParam);
                    collection.findOne({
                        _id: id
                    }, function (err, document) {
                        if (err !== null) {
                            next(err);
                        } else {
                            if (document === null) {
                             // Route to 404 handler.
                                next();
                            } else {
                                res.send(document);
                                req.db.close();
                            }
                        }
                    });
                } catch (e) {
                 // Route to 404 handler.
                    next();
                }
            }
        });
    });

    app.get("/:collection/:document/:field", function (req, res, next) {
        var collectionParam, documentParam, fieldParam;
        collectionParam = req.params.collection;
        documentParam = req.params.document;
        fieldParam = req.params.field;
        req.db.collection(collectionParam, function (err, collection) {
            var id, fields;
            if (err !== null) {
                next(err);
            } else {
                try {
                    id = new mongo.ObjectID(documentParam);
                    fields = {};
                    fields[fieldParam] = 1;
                    collection.findOne({
                        _id: id
                    }, {
                        fields: fields
                    }, function (err, document) {
                        if (err !== null) {
                            next(err);
                        } else {
                            if (document === null || document.hasOwnProperty(fieldParam) === false) {
                             // Route to 404 handler.
                                next();
                            } else {
                                res.send(document[fieldParam]);
                                req.db.close();
                            }
                        }
                    });
                } catch (e) {
                 // Route to 404 handler.
                    next();
                }
            }
        });
    });

    app.post("/:collection/query", express.json(), function (req, res) {
        var collectionParam;
        collectionParam = req.params.collection;
        req.db.collection(collectionParam, function (err, collection) {
            var options;
            if (err !== null) {
                next(err);
            } else {
                options = {};
                if (req.query.limit) {
                    options.limit = req.query.limit;
                }
                if (req.query.skip) {
                    options.skip = req.query.skip;
                }
                if (req.query.sort) {
                    options.sort = req.query.sort;
                }
                collection.find(req.body, options).toArray(function (err, documents) {
                    if (err !== null) {
                        next(err);
                    } else {
                        res.send(documents);
                        req.db.close();
                    }
                });
            }
        });
    });

    app.post("/:prefix.files", binaryBodyParser, function (req, res, next) {
        var prefixParam, contentType, grid;
        prefixParam = req.params.prefix;
        contentType = req.headers["content-type"];
        grid = new mongo.Grid(req.db, prefixParam);
        grid.put(req.body, {
            content_type: contentType
        }, function (err, document) {
            if (err !== null) {
                next(err);
            } else {
                res.location(prefixParam + ".files/" + document._id.toHexString());
                res.send(201, "Created");
                req.db.close();
            }
        });
    });

    app.post("/:collection", express.json(), function (req, res) {
        var collectionParam, payload;
        collectionParam = req.params.collection;
        payload = req.body;
        req.db.collection(collectionParam, function (err, collection) {
            if (err !== null) {
                next(err);
            } else {
                collection.insert(payload, function (err, document) {
                    if (err !== null) {
                        next(err);
                    } else {
                        res.location(collectionParam + "/" + document[0]._id.toHexString());
                        res.send(201, "Created");
                        req.db.close();
                    }
                });
            }
        });
    });

    app.patch("/:collection/:document", express.json(), function (req, res, next) {
        var collectionParam, documentParam;
        collectionParam = req.params.collection;
        documentParam = req.params.document;
        req.db.collection(collectionParam, function (err, collection) {
            var id;
            if (err !== null) {
                next(err);
            } else {
                try {
                    id = new mongo.ObjectID(documentParam);
                    collection.update({
                        _id: id
                    }, req.body, function (err) {
                        if (err !== null) {
                            next(err);
                        } else {
                            res.send(204, "No Content");
                            req.db.close();
                        }
                    });
                } catch (e) {
                 // Route to 404 handler.
                    next();
                }
            }
        });
    });

    app.delete("/:prefix.files/:file", function (req, res, next) {
        var prefixParam, fileParam, grid, id;
        prefixParam = req.params.prefix;
        fileParam = req.params.file;
        grid = new mongo.Grid(req.db, prefixParam);
        try {
            id = new mongo.ObjectID(fileParam);
            grid.delete(id, function (err, flag) {
                if (err !== null) {
                    next(err);
                } else {
                 // TODO: Detect if file was actually deleted.
                    res.send(200, "OK");
                    req.db.close();
                }
            });
        } catch (e) {
         // Route to 404 handler.
            next();
        }
    });

    app.delete("/:collection/:document", function (req, res, next) {
        var collectionParam, documentParam;
        collectionParam = req.params.collection;
        documentParam = req.params.document;
        req.db.collection(collectionParam, function (err, collection) {
            var id;
            if (err !== null) {
                next(err);
            } else {
                try {
                    id = new mongo.ObjectID(documentParam);
                    collection.remove({
                        _id: id
                    }, function (err, num) {
                        if (err !== null) {
                            next(err);
                        } else {
                            if (num === 0) {
                             // Route to 404 handler.
                                next();
                            } else {
                                res.send(200, "Deleted");
                                req.db.close();
                            }
                        }
                    });
                } catch (e) {
                 // Route to 404 handler.
                    next();
                }
            }
        });
    });

 // 404 handler.
    app.use(function (req, res) {
        req.db.close();
        res.send(404, "Not Found");
    });

 // 500 handler.
    app.use(function (err, req, res, next) {
        if (req.hasOwnProperty("db") === true) {
            req.db.close();
        }
        res.send(500, "Internal Server Error");
    });

    app.listen(argv.port);

    console.log("Ming is running on port " + argv.port + ", connected to " + argv["connection-string"]);

}());
<commit_msg>Deploy routes manually.<commit_after>(function () {
    "use strict";

    var argv, express, corser, mongo, url, binaryBodyParser, app;

    argv = require("optimist")
             .options("port", {
                 default: 1337,
                 describe: "Port"
             })
             .options("connection-string", {
                 default: "mongodb://localhost/ming",
                 describe: "MongoDB Connection String for the default deployment."
             })
             .options("enable-proxying", {
                 default: false,
                 describe: "Allow connections to other MongoDB deployments."
             })
             .argv;
    express = require("express");
    corser = require("corser");
    mongo = require("mongodb");
    url = require("url");

    binaryBodyParser = function (req, res, next) {
        var body;
        body = new Buffer(0);
        req.on("data", function (chunk) {
            var buffer;
            buffer = new Buffer(body.length + chunk.length);
            body.copy(buffer);
            chunk.copy(buffer, body.length);
            body = buffer;
        });
        req.on("end", function () {
            req.body = body;
            next();
        });
    };

    app = express();

    app.configure(function () {

     // Handle CORS.
        app.use(corser.create({
            methods: corser.simpleMethods.concat(["DELETE", "PATCH"]),
            requestHeaders: corser.simpleRequestHeaders.concat(["Authorization", "X-Connection-String"]),
            responseHeaders: corser.simpleResponseHeaders.concat(["Location"])
        }));

     // Terminate CORS preflights.
        app.use(function (req, res, next) {
            if (req.method === "OPTIONS") {
                res.writeHead(204);
                res.end();
            } else {
                next();
            }
        });

     // Prepare MongoDB client.
        app.use(function (req, res, next) {
            var connectionString;
            if (argv["enable-proxying"] === true && req.headers.hasOwnProperty("x-connection-string") === true) {
                connectionString = req.headers["x-connection-string"];
            } else {
                connectionString = argv["connection-string"];
            }
            req.connectionString = url.parse(connectionString);
            mongo.MongoClient.connect(connectionString, function (err, db) {
                if (err !== null) {
                    next(err);
                } else {
                    req.db = db;
                    next();
                }
            });
        });

     // Basic auth.
        app.use(function (req, res, next) {
            var basicAuth;
            basicAuth = express.basicAuth(function (username, password, callback) {
                req.db.authenticate(username, password, function (err) {
                    if (err !== null) {
                     // Close database if authentication fails.
                        req.db.close();
                        callback(err, null);
                    } else {
                        callback(null, username);
                    }
                });
            }, "MongoDB");
            basicAuth(req, res, next);
        });

     // Deploy routes.
        app.use(app.router);

     // 404 handler.
        app.use(function (req, res) {
            req.db.close();
            res.send(404, "Not Found");
        });

     // 500 handler.
        app.use(function (err, req, res, next) {
            if (req.hasOwnProperty("db") === true) {
                req.db.close();
            }
            res.send(500, "Internal Server Error");
        });

    });

    app.get("/", function (req, res) {
        req.db.collectionNames({
            namesOnly: true
        }, function (err, collections) {
            var names;
            if (err !== null) {
                next(err);
            } else {
                names = collections.map(function (collection) {
                 // Strip database name.
                    return collection.substring(req.connectionString.path.length);
                });
                res.send({
                    collections: names
                });
                req.db.close();
            }
        });
    });

    app.get("/:collection", function (req, res) {
        var collectionParam;
        collectionParam = req.params.collection;
        req.db.collection(collectionParam, function (err, collection) {
            if (err !== null) {
                next(err);
            } else {
                collection.find().count(function (err, count) {
                    if (err !== null) {
                        next(err);
                    } else {
                        res.send({count: count});
                        req.db.close();
                    }
                });
            }
        });
    });

    app.get("/:prefix.files/:file", function (req, res, next) {
        var prefixParam, fileParam;
        prefixParam = req.params.prefix;
        fileParam = req.params.file;
        req.db.collection(prefixParam + ".files", function (err, collection) {
            var id;
            if (err !== null) {
                next(err);
            } else {
                try {
                    id = new mongo.ObjectID(fileParam);
                 // Get metadata first.
                    collection.findOne({
                        _id: id
                    }, function (err, document) {
                        var grid;
                        if (err !== null) {
                            next(err);
                        } else {
                            if (document === null) {
                             // Route to 404 handler.
                                next();
                            } else {
                                if (req.query.hasOwnProperty("binary") === true && req.query.binary === "true") {
                                    grid = new mongo.Grid(req.db, prefixParam);
                                    grid.get(id, function (err, file) {
                                        var contentType;
                                        if (err !== null) {
                                            next(err);
                                        } else {
                                            if (document.contentType) {
                                                contentType = document.contentType;
                                            } else if (document.filename) {
                                             // Guess file type from file extension.
                                                contentType = document.filename.match(/(\.\w+)$/)[1];
                                            } else {
                                                contentType = "application/octet-stream";
                                            }
                                            res.type(contentType);
                                            res.send(file);
                                            req.db.close();
                                        }
                                    });
                                } else {
                                    res.send(document);
                                    req.db.close();
                                }
                            }
                        }
                    });
                } catch (e) {
                 // Route to 404 handler.
                    next();
                }
            }
        });
    });

    app.get("/:collection/:document", function (req, res, next) {
        var collectionParam, documentParam;
        collectionParam = req.params.collection;
        documentParam = req.params.document;
        req.db.collection(collectionParam, function (err, collection) {
            var id;
            if (err !== null) {
                next(err);
            } else {
                try {
                    id = new mongo.ObjectID(documentParam);
                    collection.findOne({
                        _id: id
                    }, function (err, document) {
                        if (err !== null) {
                            next(err);
                        } else {
                            if (document === null) {
                             // Route to 404 handler.
                                next();
                            } else {
                                res.send(document);
                                req.db.close();
                            }
                        }
                    });
                } catch (e) {
                 // Route to 404 handler.
                    next();
                }
            }
        });
    });

    app.get("/:collection/:document/:field", function (req, res, next) {
        var collectionParam, documentParam, fieldParam;
        collectionParam = req.params.collection;
        documentParam = req.params.document;
        fieldParam = req.params.field;
        req.db.collection(collectionParam, function (err, collection) {
            var id, fields;
            if (err !== null) {
                next(err);
            } else {
                try {
                    id = new mongo.ObjectID(documentParam);
                    fields = {};
                    fields[fieldParam] = 1;
                    collection.findOne({
                        _id: id
                    }, {
                        fields: fields
                    }, function (err, document) {
                        if (err !== null) {
                            next(err);
                        } else {
                            if (document === null || document.hasOwnProperty(fieldParam) === false) {
                             // Route to 404 handler.
                                next();
                            } else {
                                res.send(document[fieldParam]);
                                req.db.close();
                            }
                        }
                    });
                } catch (e) {
                 // Route to 404 handler.
                    next();
                }
            }
        });
    });

    app.post("/:collection/query", express.json(), function (req, res) {
        var collectionParam;
        collectionParam = req.params.collection;
        req.db.collection(collectionParam, function (err, collection) {
            var options;
            if (err !== null) {
                next(err);
            } else {
                options = {};
                if (req.query.limit) {
                    options.limit = req.query.limit;
                }
                if (req.query.skip) {
                    options.skip = req.query.skip;
                }
                if (req.query.sort) {
                    options.sort = req.query.sort;
                }
                collection.find(req.body, options).toArray(function (err, documents) {
                    if (err !== null) {
                        next(err);
                    } else {
                        res.send(documents);
                        req.db.close();
                    }
                });
            }
        });
    });

    app.post("/:prefix.files", binaryBodyParser, function (req, res, next) {
        var prefixParam, contentType, grid;
        prefixParam = req.params.prefix;
        contentType = req.headers["content-type"];
        grid = new mongo.Grid(req.db, prefixParam);
        grid.put(req.body, {
            content_type: contentType
        }, function (err, document) {
            if (err !== null) {
                next(err);
            } else {
                res.location(prefixParam + ".files/" + document._id.toHexString());
                res.send(201, "Created");
                req.db.close();
            }
        });
    });

    app.post("/:collection", express.json(), function (req, res) {
        var collectionParam, payload;
        collectionParam = req.params.collection;
        payload = req.body;
        req.db.collection(collectionParam, function (err, collection) {
            if (err !== null) {
                next(err);
            } else {
                collection.insert(payload, function (err, document) {
                    if (err !== null) {
                        next(err);
                    } else {
                        res.location(collectionParam + "/" + document[0]._id.toHexString());
                        res.send(201, "Created");
                        req.db.close();
                    }
                });
            }
        });
    });

    app.patch("/:collection/:document", express.json(), function (req, res, next) {
        var collectionParam, documentParam;
        collectionParam = req.params.collection;
        documentParam = req.params.document;
        req.db.collection(collectionParam, function (err, collection) {
            var id;
            if (err !== null) {
                next(err);
            } else {
                try {
                    id = new mongo.ObjectID(documentParam);
                    collection.update({
                        _id: id
                    }, req.body, function (err) {
                        if (err !== null) {
                            next(err);
                        } else {
                            res.send(204, "No Content");
                            req.db.close();
                        }
                    });
                } catch (e) {
                 // Route to 404 handler.
                    next();
                }
            }
        });
    });

    app.delete("/:prefix.files/:file", function (req, res, next) {
        var prefixParam, fileParam, grid, id;
        prefixParam = req.params.prefix;
        fileParam = req.params.file;
        grid = new mongo.Grid(req.db, prefixParam);
        try {
            id = new mongo.ObjectID(fileParam);
            grid.delete(id, function (err, flag) {
                if (err !== null) {
                    next(err);
                } else {
                 // TODO: Detect if file was actually deleted.
                    res.send(200, "OK");
                    req.db.close();
                }
            });
        } catch (e) {
         // Route to 404 handler.
            next();
        }
    });

    app.delete("/:collection/:document", function (req, res, next) {
        var collectionParam, documentParam;
        collectionParam = req.params.collection;
        documentParam = req.params.document;
        req.db.collection(collectionParam, function (err, collection) {
            var id;
            if (err !== null) {
                next(err);
            } else {
                try {
                    id = new mongo.ObjectID(documentParam);
                    collection.remove({
                        _id: id
                    }, function (err, num) {
                        if (err !== null) {
                            next(err);
                        } else {
                            if (num === 0) {
                             // Route to 404 handler.
                                next();
                            } else {
                                res.send(200, "Deleted");
                                req.db.close();
                            }
                        }
                    });
                } catch (e) {
                 // Route to 404 handler.
                    next();
                }
            }
        });
    });

    app.listen(argv.port);

    console.log("Ming is running on port " + argv.port + ", connected to " + argv["connection-string"]);

}());
<|endoftext|>


<commit_before>var http = require('http')
var cadence = require('cadence')
var Signal = require('signal')
var util = require('util')
var events = require('events')
var descendent = require('foremost')('descendent')

var coalesce = require('extant')

function Mock () {
    this._pid = 0
    var ready = this.ready = new Signal
    descendent.on('olio:mock', function () { ready.unlatch() })
}

Mock.prototype.initialize = function (argv, index) {
    descendent.across('olio:message', {
        method: 'factory'
    }, this)
    descendent.across('olio:message', {
        method: 'initialize',
        argv: argv,
        index: index
    })
}

Mock.prototype.sender = function (name, index, sender) {
    descendent.across('olio:message', {
        method: 'connect',
        from: { name: name, index: index }
    }, coalesce(sender))
    return sender
}

Mock.prototype.sibling = function (name, count, factory) {
    var paths = []
    for (var i = 0; i < count; i++) {
        paths[i] = [ 0, ++this._pid ]
    }
    descendent.across('olio:message', {
        method: 'created',
        paths: paths,
        name: name,
        argv: [],
        socketPath: null,
        count: count
    }, factory)
}

Mock.prototype.createReceiver = cadence(function (async, destructible, Receiver, message, sender) {
    async(function () {
        destructible.monitor('receiver', Receiver, message.from, message.to, async())
    }, function (receiver) {
        destructible.destruct.wait(function () { receiver.inbox.push(null) })
        sender.outbox.pump(receiver.inbox)
        receiver.outbox.pump(sender.inbox)
    })
})

Mock.prototype.createSender = cadence(function (async, destructible, from, Receiver, message, Sibling, index) {
    async(function () {
        destructible.monitor('sibling', Sibling, index, message.count, async())
    }, function (sibling) {
        destructible.destruct.wait(function () { sibling.inbox.push(null) })
        async(function () {
            destructible.monitor('receiver', Receiver, message.argv, index, message.count, async())
        }, function (receiver) {
            receiver.outbox.pump(sibling.inbox)
            destructible.destruct.wait(function () { receiver.inbox.push(null) })
            sibling.outbox.pump(receiver.inbox)
        })
    })
})

module.exports = Mock
<commit_msg>Do not mock with the special zero pid.<commit_after>var http = require('http')
var cadence = require('cadence')
var Signal = require('signal')
var util = require('util')
var events = require('events')
var descendent = require('foremost')('descendent')

var coalesce = require('extant')

function Mock () {
    this._pid = descendent.process.pid
    var ready = this.ready = new Signal
    descendent.on('olio:mock', function () { ready.unlatch() })
}

Mock.prototype.initialize = function (argv, index) {
    descendent.across('olio:message', {
        method: 'factory'
    }, this)
    descendent.across('olio:message', {
        method: 'initialize',
        argv: argv,
        index: index
    })
}

Mock.prototype.sender = function (name, index, sender) {
    descendent.across('olio:message', {
        method: 'connect',
        from: { name: name, index: index }
    }, coalesce(sender))
    return sender
}

Mock.prototype.sibling = function (name, count, factory) {
    var pid = ++this._pid
    descendent.addMockChild(pid, null)
    var paths = []
    for (var i = 0; i < count; i++) {
        paths[i] = [ descendent.process.pid, this._pid ]
    }
    descendent.across('olio:message', {
        method: 'created',
        paths: paths,
        name: name,
        argv: [],
        socketPath: null,
        count: count
    }, factory)
}

Mock.prototype.createReceiver = cadence(function (async, destructible, Receiver, message, sender) {
    async(function () {
        destructible.monitor('receiver', Receiver, message.from, message.to, async())
    }, function (receiver) {
        destructible.destruct.wait(function () { receiver.inbox.push(null) })
        sender.outbox.pump(receiver.inbox)
        receiver.outbox.pump(sender.inbox)
    })
})

Mock.prototype.createSender = cadence(function (async, destructible, from, Receiver, message, Sibling, index) {
    async(function () {
        destructible.monitor('sibling', Sibling, index, message.count, async())
    }, function (sibling) {
        destructible.destruct.wait(function () { sibling.inbox.push(null) })
        async(function () {
            destructible.monitor('receiver', Receiver, message.argv, index, message.count, async())
        }, function (receiver) {
            receiver.outbox.pump(sibling.inbox)
            destructible.destruct.wait(function () { receiver.inbox.push(null) })
            sibling.outbox.pump(receiver.inbox)
        })
    })
})

module.exports = Mock
<|endoftext|>


<commit_before>module.exports = require('./build/_mock.js')
<commit_msg>Fix mock link<commit_after>module.exports = require('./dist/_mock.js')
<|endoftext|>


<commit_before>// @flow strict-local

/**
 * The data we need in JS/React code for acting on a notification.
 *
 * The actual objects we describe with these types may have a bunch more
 * fields.  So, properly, these object types aren't really exact.  But
 * pretending they are seems to be the least unpleasant way to get Flow to
 * recognize the effect of refinements like `data.pm_users === undefined`.
 *
 * Currently:
 *
 *  * On iOS, these objects are translated directly, field by field, from
 *    the blobs of key/value pairs sent by the Zulip server in the
 *    "payload".  The set of fields therefore varies by server version.
 *
 *  * On Android, our platform-native code constructs the exact data to
 *    send; see `MessageFcmMessage#dataForOpen` in `FcmMessage.kt`.
 *    That should be kept in sync with this type definition, in which case
 *    these types really are exact.
 */
export type Notification =
  | {| recipient_type: 'stream', stream: string, topic: string, realm_uri?: string |}
  // Group PM messages have `pm_users`, which is comma-separated IDs.
  | {| recipient_type: 'private', pm_users: string, realm_uri?: string |}
  // 1:1 PM messages lack `pm_users`.
  | {| recipient_type: 'private', sender_email: string, realm_uri?: string |};
<commit_msg>notifications [nfc]: Update `Notification` type's JSDoc.<commit_after>// @flow strict-local

/**
 * The data we need in JS/React code for acting on a notification.
 *
 * Currently:
 *  * On iOS, these objects are reconstructed in React Native from the APNs
 *    payload; the exactness of these type definitions is checked by Flow. (See
 *    `fromAPNs` in `src/notifications/extract.js`.)
 *
 *  * On Android, these objects are constructed by casting JSON objects from our
 *    platform-native code; they are exact iff that code is kept in sync. (See
 *    `MessageFcmMessage#dataForOpen` in `FcmMessage.kt`.)
 */
export type Notification =
  | {| recipient_type: 'stream', stream: string, topic: string, realm_uri?: string |}
  // Group PM messages have `pm_users`, which is comma-separated IDs.
  | {| recipient_type: 'private', pm_users: string, realm_uri?: string |}
  // 1:1 PM messages lack `pm_users`.
  | {| recipient_type: 'private', sender_email: string, realm_uri?: string |};
<|endoftext|>


<commit_before>import {EventEmitter} from 'events';
import {formatLivethreadPermissions, handleJsonErrors, isBrowser} from '../helpers.js';
import RedditContent from './RedditContent.js';

const WebSocket = isBrowser ? global.WebSocket : require('ws');

const api_type = 'json';

/**
* A class representing a live reddit thread
* <style> #LiveThread {display: none} </style>
* @example
*
* // Get a livethread with the given ID
* r.getLivethread('whrdxo8dg9n0')
* @desc For the most part, reddit distributes the content of live threads via websocket, rather than through the REST API.
As such, snoowrap assigns each fetched LiveThread object a `stream` property, which takes the form of an
[EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter). To listen for new thread updates, simply
add listeners to that emitter.

The following events can be emitted:
- `update`: Occurs when a new update has been posted in this thread. Emits a `LiveUpdate` object containing information
about the new update.
- `activity`: Occurs periodically when the viewer count for this thread changes.
- `settings`: Occurs when the thread's settings change. Emits an object containing the new settings.
- `delete`: Occurs when an update has been deleted. Emits the ID of the deleted update.
- `strike`: Occurs when an update has been striken (marked incorrect and crossed out). Emits the ID of the striken update.
- `embeds_ready`: Occurs when embedded media is now available for a previously-posted update.
- `complete`: Occurs when this LiveThread has been marked as complete, and no more updates will be sent.

(Note: These event types are mapped directly from reddit's categorization of the updates. The descriptions above are
paraphrased from reddit's descriptions [here](https://www.reddit.com/dev/api#section_live).)

As an example, this would log all new livethread updates to the console:

```javascript
someLivethread.stream.on('update', data => {
  console.log(data.body);
});
```

* @extends RedditContent
*/
const LiveThread = class LiveThread extends RedditContent {
  get _uri () {
    return `live/${this.id}/about`;
  }
  _transformApiResponse (response) {
    response._rawStream = null;
    response._populatedStream = new EventEmitter();
    Object.defineProperty(response, 'stream', {get: () => {
      if (!response._rawStream) {
        response._rawStream = new WebSocket(response.websocket_url);
        const handler = data => {
          const parsed = this._r._populate(JSON.parse(data));
          response._populatedStream.emit(parsed.type, parsed.payload);
        };
        if (typeof response._rawStream.on === 'function') {
          response._rawStream.on('message', handler);
        } else {
          response._rawStream.onmessage = messageEvent => handler(messageEvent.data);
        }
      }
      return response._populatedStream;
    }});
    return response;
  }
  /**
  * @summary Adds a new update to this thread.
  * @param {string} body The body of the new update
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').addUpdate('Breaking: Someone is reading the snoowrap documentation \\o/')
  */
  addUpdate (body) {
    return this._post({uri: `api/live/${this.id}/update`, form: {api_type, body}}).then(handleJsonErrors(this));
  }
  /**
  * @summary Strikes (marks incorrect and crosses out) the given update.
  * @param {object} options
  * @param {string} options.id The ID of the update that should be striked.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').strikeUpdate({id: 'LiveUpdate_edc34446-faf0-11e5-a1b4-0e858bca33cd'})
  */
  strikeUpdate ({id}) {
    return this._post({
      uri: `api/live/${this.id}/strike_update`,
      form: {api_type, id: `${id.startsWith('LiveUpdate_') ? '' : 'LiveUpdate_'}${id}`}
    }).then(handleJsonErrors(this));
  }
  /**
  * @summary Deletes an update from this LiveThread.
  * @param {object} options
  * @param {string} options.id The ID of the LiveUpdate that should be deleted
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').deleteUpdate({id: 'LiveUpdate_edc34446-faf0-11e5-a1b4-0e858bca33cd'})
  */
  deleteUpdate ({id}) {
    return this._post({
      uri: `api/live/${this.id}/delete_update`,
      form: {api_type, id: `${id.startsWith('LiveUpdate_') ? '' : 'LiveUpdate_'}${id}`}
    }).then(handleJsonErrors(this));
  }
  /**
  * @summary Gets a list of this LiveThread's contributors
  * @returns {Promise} An Array containing RedditUsers
  * @example
  *
  * r.getLivethread('whrdxo8dg9n0').getContributors().then(console.log)
  * // => [
  * //  RedditUser { permissions: ['edit'], name: 'not_an_aardvark', id: 't2_k83md' },
  * //  RedditUser { permissions: ['all'], id: 't2_u3l80', name: 'snoowrap_testing' }
  * // ]
  */
  getContributors () {
    return this._get({uri: `live/${this.id}/contributors`}).then(contributors => {
      return Array.isArray(contributors[0]) ? contributors[0] : contributors;
    });
  }
  /**
  * @summary Invites a contributor to this LiveThread.
  * @param {object} options
  * @param {string} options.name The name of the user who should be invited
  * @param {Array} options.permissions The permissions that the invited user should receive. This should be an Array containing
  some combination of `'update', 'edit', 'manage'`. To invite a contributor with full permissions, omit this property.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').inviteContributor({name: 'actually_an_aardvark', permissions: ['update']})
  */
  inviteContributor ({name, permissions}) {
    return this._post({uri: `api/live/${this.id}/invite_contributor`, form: {
      api_type,
      name,
      permissions: formatLivethreadPermissions(permissions),
      type: 'liveupdate_contributor_invite'
    }}).then(handleJsonErrors(this));
  }
  /**
  * @summary Revokes an invitation for the given user to become a contributor on this LiveThread.
  * @param {object} options
  * @param {string} options.name The username of the account whose invitation should be revoked
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').revokeContributorInvite({name: 'actually_an_aardvark'});
  */
  revokeContributorInvite ({name}) {
    return this._r.getUser(name).fetch().get('id').then(userId => {
      return this._post({uri: `api/live/${this.id}/rm_contributor_invite`, form: {api_type, id: `t2_${userId}`}});
    }).then(handleJsonErrors(this));
  }
  /**
  * @summary Accepts a pending contributor invitation on this LiveThread.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').acceptContributorInvite()
  */
  acceptContributorInvite () {
    return this._post({uri: `api/live/${this.id}/accept_contributor_invite`, form: {api_type}}).return(this);
  }
  /**
  * @summary Abdicates contributor status on this LiveThread.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').leaveContributor()
  */
  leaveContributor () {
    return this._post({uri: `api/live/${this.id}/leave_contributor`, form: {api_type}}).return(this);
  }
  /**
  * @summary Removes the given user from contributor status on this LiveThread.
  * @param {object} options
  * @param {string} options.name The username of the account who should be removed
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').removeContributor({name: 'actually_an_aardvark'})
  */
  removeContributor ({name}) {
    return this._r.getUser(name).fetch().get('id').then(userId => {
      return this._post({uri: `api/live/${this.id}/rm_contributor`, form: {api_type, id: `t2_${userId}`}});
    }).then(handleJsonErrors(this));
  }
  /**
  * @summary Sets the permissions of the given contributor.
  * @param {object} options
  * @param {string} options.name The name of the user whose permissions should be changed
  * @param {Array} options.permissions The updated permissions that the user should have. This should be an Array containing
  some combination of `'update', 'edit', 'manage'`. To give the contributor with full permissions, omit this property.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').setContributorPermissions({name: 'actually_an_aardvark', permissions: ['edit']})
  */
  setContributorPermissions ({name, permissions}) {
    return this._post({
      uri: `api/live/${this.id}/set_contributor_permissions`,
      form: {api_type, name, permissions: formatLivethreadPermissions(permissions), type: 'liveupdate_contributor'}
    }).then(handleJsonErrors(this));
  }
  /**
  * @summary Edits the settings on this LiveThread.
  * @param {object} options
  * @param {string} options.title The title of the thread
  * @param {string} [options.description] A descriptions of the thread. 120 characters max
  * @param {string} [options.resources] Information and useful links related to the thread. 120 characters max
  * @param {boolean} options.nsfw Determines whether the thread is Not Safe For Work
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').editSettings({title: 'My livethread', description: 'an updated description'})
  */
  editSettings ({title, description, resources, nsfw}) {
    return this._post({
      uri: `api/live/${this.id}/edit`,
      form: {api_type, description, nsfw, resources, title}
    }).then(handleJsonErrors(this));
  }
  /**
  * @summary Permanently closes this thread, preventing any more updates from being added.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').closeThread()
  */
  closeThread () {
    return this._post({uri: `api/live/${this.id}/close_thread`, form: {api_type}}).return(this);
  }
  /**
  * @summary Reports this LiveThread for breaking reddit's rules.
  * @param {object} options
  * @param {string} options.reason The reason for the report. One of `spam`, `vote-manipulation`, `personal-information`,
  `sexualizing-minors`, `site-breaking`
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').report({reason: 'Breaking a rule blah blah blah'})
  */
  report ({reason}) {
    return this._post({uri: `api/live/${this.id}/report`, form: {api_type, type: reason}}).then(handleJsonErrors(this));
  }
  /**
  * @summary Gets a Listing containing past updates to this LiveThread.
  * @param {object} [options] Options for the resulting Listing
  * @returns {Promise} A Listing containing LiveUpdates
  * @example
  *
  * r.getLivethread('whrdxo8dg9n0').getRecentUpdates().then(console.log)
  * // => Listing [
  * //  LiveUpdate { ... },
  * //  LiveUpdate { ... },
  * //  ...
  * // ]
  */
  getRecentUpdates (options) {
    return this._getListing({uri: `live/${this.id}`, qs: options});
  }
  /**
  * @summary Gets a list of reddit submissions linking to this LiveThread.
  * @param {object} [options] Options for the resulting Listing
  * @returns {Promise} A Listing containing Submissions
  * @example
  *
  * r.getLivethread('whrdxo8dg9n0').getDiscussions().then(console.log)
  * // => Listing [
  * //  Submission { ... },
  * //  Submission { ... },
  * //  ...
  * // ]
  */
  getDiscussions (options) {
    return this._getListing({uri: `live/${this.id}/discussions`, qs: options});
  }
  /**
  * @summary Stops listening for new updates on this LiveThread.
  * @desc To avoid memory leaks that can result from open sockets, it's recommended that you call this method when you're
  finished listening for updates on this LiveThread.
  *
  * This should not be confused with {@link LiveThread#closeThread}, which marks the thread as "closed" on reddit.
  * @returns undefined
  * @example
  *
  * var myThread = r.getLivethread('whrdxo8dg9n0');
  * myThread.stream.on('update', content => {
  *   console.log(content);
  *   myThread.closeStream();
  * })
  *
  */
  closeStream () {
    this._rawStream.close();
  }
};

export default LiveThread;
<commit_msg>Allow livethreads to be streamed if received from other endpoints<commit_after>import {EventEmitter} from 'events';
import {formatLivethreadPermissions, handleJsonErrors, isBrowser} from '../helpers.js';
import RedditContent from './RedditContent.js';

const WebSocket = isBrowser ? global.WebSocket : require('ws');

const api_type = 'json';

/**
* A class representing a live reddit thread
* <style> #LiveThread {display: none} </style>
* @example
*
* // Get a livethread with the given ID
* r.getLivethread('whrdxo8dg9n0')
* @desc For the most part, reddit distributes the content of live threads via websocket, rather than through the REST API.
As such, snoowrap assigns each fetched LiveThread object a `stream` property, which takes the form of an
[EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter). To listen for new thread updates, simply
add listeners to that emitter.

The following events can be emitted:
- `update`: Occurs when a new update has been posted in this thread. Emits a `LiveUpdate` object containing information
about the new update.
- `activity`: Occurs periodically when the viewer count for this thread changes.
- `settings`: Occurs when the thread's settings change. Emits an object containing the new settings.
- `delete`: Occurs when an update has been deleted. Emits the ID of the deleted update.
- `strike`: Occurs when an update has been striken (marked incorrect and crossed out). Emits the ID of the striken update.
- `embeds_ready`: Occurs when embedded media is now available for a previously-posted update.
- `complete`: Occurs when this LiveThread has been marked as complete, and no more updates will be sent.

(Note: These event types are mapped directly from reddit's categorization of the updates. The descriptions above are
paraphrased from reddit's descriptions [here](https://www.reddit.com/dev/api#section_live).)

As an example, this would log all new livethread updates to the console:

```javascript
someLivethread.stream.on('update', data => {
  console.log(data.body);
});
```

* @extends RedditContent
*/
const LiveThread = class LiveThread extends RedditContent {
  constructor (options, _r, _hasFetched) {
    super(options, _r, _hasFetched);
    this._rawStream = null;
    this._populatedStream = null;
    if (_hasFetched) {
      Object.defineProperty(this, 'stream', {get: () => {
        if (!this._populatedStream) {
          this._setupWebSocket();
        }
        return this._populatedStream;
      }});
    }
  }
  get _uri () {
    return `live/${this.id}/about`;
  }
  _setupWebSocket () {
    this._rawStream = new WebSocket(this.websocket_url);
    this._populatedStream = new EventEmitter();
    const handler = data => {
      const parsed = this._r._populate(JSON.parse(data));
      this._populatedStream.emit(parsed.type, parsed.payload);
    };
    if (typeof this._rawStream.on === 'function') {
      this._rawStream.on('message', handler);
    } else {
      this._rawStream.onmessage = messageEvent => handler(messageEvent.data);
    }
  }
  /**
  * @summary Adds a new update to this thread.
  * @param {string} body The body of the new update
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').addUpdate('Breaking: Someone is reading the snoowrap documentation \\o/')
  */
  addUpdate (body) {
    return this._post({uri: `api/live/${this.id}/update`, form: {api_type, body}}).then(handleJsonErrors(this));
  }
  /**
  * @summary Strikes (marks incorrect and crosses out) the given update.
  * @param {object} options
  * @param {string} options.id The ID of the update that should be striked.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').strikeUpdate({id: 'LiveUpdate_edc34446-faf0-11e5-a1b4-0e858bca33cd'})
  */
  strikeUpdate ({id}) {
    return this._post({
      uri: `api/live/${this.id}/strike_update`,
      form: {api_type, id: `${id.startsWith('LiveUpdate_') ? '' : 'LiveUpdate_'}${id}`}
    }).then(handleJsonErrors(this));
  }
  /**
  * @summary Deletes an update from this LiveThread.
  * @param {object} options
  * @param {string} options.id The ID of the LiveUpdate that should be deleted
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').deleteUpdate({id: 'LiveUpdate_edc34446-faf0-11e5-a1b4-0e858bca33cd'})
  */
  deleteUpdate ({id}) {
    return this._post({
      uri: `api/live/${this.id}/delete_update`,
      form: {api_type, id: `${id.startsWith('LiveUpdate_') ? '' : 'LiveUpdate_'}${id}`}
    }).then(handleJsonErrors(this));
  }
  /**
  * @summary Gets a list of this LiveThread's contributors
  * @returns {Promise} An Array containing RedditUsers
  * @example
  *
  * r.getLivethread('whrdxo8dg9n0').getContributors().then(console.log)
  * // => [
  * //  RedditUser { permissions: ['edit'], name: 'not_an_aardvark', id: 't2_k83md' },
  * //  RedditUser { permissions: ['all'], id: 't2_u3l80', name: 'snoowrap_testing' }
  * // ]
  */
  getContributors () {
    return this._get({uri: `live/${this.id}/contributors`}).then(contributors => {
      return Array.isArray(contributors[0]) ? contributors[0] : contributors;
    });
  }
  /**
  * @summary Invites a contributor to this LiveThread.
  * @param {object} options
  * @param {string} options.name The name of the user who should be invited
  * @param {Array} options.permissions The permissions that the invited user should receive. This should be an Array containing
  some combination of `'update', 'edit', 'manage'`. To invite a contributor with full permissions, omit this property.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').inviteContributor({name: 'actually_an_aardvark', permissions: ['update']})
  */
  inviteContributor ({name, permissions}) {
    return this._post({uri: `api/live/${this.id}/invite_contributor`, form: {
      api_type,
      name,
      permissions: formatLivethreadPermissions(permissions),
      type: 'liveupdate_contributor_invite'
    }}).then(handleJsonErrors(this));
  }
  /**
  * @summary Revokes an invitation for the given user to become a contributor on this LiveThread.
  * @param {object} options
  * @param {string} options.name The username of the account whose invitation should be revoked
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').revokeContributorInvite({name: 'actually_an_aardvark'});
  */
  revokeContributorInvite ({name}) {
    return this._r.getUser(name).fetch().get('id').then(userId => {
      return this._post({uri: `api/live/${this.id}/rm_contributor_invite`, form: {api_type, id: `t2_${userId}`}});
    }).then(handleJsonErrors(this));
  }
  /**
  * @summary Accepts a pending contributor invitation on this LiveThread.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').acceptContributorInvite()
  */
  acceptContributorInvite () {
    return this._post({uri: `api/live/${this.id}/accept_contributor_invite`, form: {api_type}}).return(this);
  }
  /**
  * @summary Abdicates contributor status on this LiveThread.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').leaveContributor()
  */
  leaveContributor () {
    return this._post({uri: `api/live/${this.id}/leave_contributor`, form: {api_type}}).return(this);
  }
  /**
  * @summary Removes the given user from contributor status on this LiveThread.
  * @param {object} options
  * @param {string} options.name The username of the account who should be removed
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').removeContributor({name: 'actually_an_aardvark'})
  */
  removeContributor ({name}) {
    return this._r.getUser(name).fetch().get('id').then(userId => {
      return this._post({uri: `api/live/${this.id}/rm_contributor`, form: {api_type, id: `t2_${userId}`}});
    }).then(handleJsonErrors(this));
  }
  /**
  * @summary Sets the permissions of the given contributor.
  * @param {object} options
  * @param {string} options.name The name of the user whose permissions should be changed
  * @param {Array} options.permissions The updated permissions that the user should have. This should be an Array containing
  some combination of `'update', 'edit', 'manage'`. To give the contributor with full permissions, omit this property.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').setContributorPermissions({name: 'actually_an_aardvark', permissions: ['edit']})
  */
  setContributorPermissions ({name, permissions}) {
    return this._post({
      uri: `api/live/${this.id}/set_contributor_permissions`,
      form: {api_type, name, permissions: formatLivethreadPermissions(permissions), type: 'liveupdate_contributor'}
    }).then(handleJsonErrors(this));
  }
  /**
  * @summary Edits the settings on this LiveThread.
  * @param {object} options
  * @param {string} options.title The title of the thread
  * @param {string} [options.description] A descriptions of the thread. 120 characters max
  * @param {string} [options.resources] Information and useful links related to the thread. 120 characters max
  * @param {boolean} options.nsfw Determines whether the thread is Not Safe For Work
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').editSettings({title: 'My livethread', description: 'an updated description'})
  */
  editSettings ({title, description, resources, nsfw}) {
    return this._post({
      uri: `api/live/${this.id}/edit`,
      form: {api_type, description, nsfw, resources, title}
    }).then(handleJsonErrors(this));
  }
  /**
  * @summary Permanently closes this thread, preventing any more updates from being added.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').closeThread()
  */
  closeThread () {
    return this._post({uri: `api/live/${this.id}/close_thread`, form: {api_type}}).return(this);
  }
  /**
  * @summary Reports this LiveThread for breaking reddit's rules.
  * @param {object} options
  * @param {string} options.reason The reason for the report. One of `spam`, `vote-manipulation`, `personal-information`,
  `sexualizing-minors`, `site-breaking`
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.getLivethread('whrdxo8dg9n0').report({reason: 'Breaking a rule blah blah blah'})
  */
  report ({reason}) {
    return this._post({uri: `api/live/${this.id}/report`, form: {api_type, type: reason}}).then(handleJsonErrors(this));
  }
  /**
  * @summary Gets a Listing containing past updates to this LiveThread.
  * @param {object} [options] Options for the resulting Listing
  * @returns {Promise} A Listing containing LiveUpdates
  * @example
  *
  * r.getLivethread('whrdxo8dg9n0').getRecentUpdates().then(console.log)
  * // => Listing [
  * //  LiveUpdate { ... },
  * //  LiveUpdate { ... },
  * //  ...
  * // ]
  */
  getRecentUpdates (options) {
    return this._getListing({uri: `live/${this.id}`, qs: options});
  }
  /**
  * @summary Gets a list of reddit submissions linking to this LiveThread.
  * @param {object} [options] Options for the resulting Listing
  * @returns {Promise} A Listing containing Submissions
  * @example
  *
  * r.getLivethread('whrdxo8dg9n0').getDiscussions().then(console.log)
  * // => Listing [
  * //  Submission { ... },
  * //  Submission { ... },
  * //  ...
  * // ]
  */
  getDiscussions (options) {
    return this._getListing({uri: `live/${this.id}/discussions`, qs: options});
  }
  /**
  * @summary Stops listening for new updates on this LiveThread.
  * @desc To avoid memory leaks that can result from open sockets, it's recommended that you call this method when you're
  finished listening for updates on this LiveThread.
  *
  * This should not be confused with {@link LiveThread#closeThread}, which marks the thread as "closed" on reddit.
  * @returns undefined
  * @example
  *
  * var myThread = r.getLivethread('whrdxo8dg9n0');
  * myThread.stream.on('update', content => {
  *   console.log(content);
  *   myThread.closeStream();
  * })
  *
  */
  closeStream () {
    if (this._rawStream) {
      this._rawStream.close();
    }
  }
};

export default LiveThread;
<|endoftext|>


<commit_before>var much = (function () {

	/**
	 * Provides requestAnimationFrame in a cross browser way.
	 * @author greggman / http://greggman.com/
	 */

	if (!window.requestAnimationFrame) {
		window.requestAnimationFrame = (function() {
			return window.webkitRequestAnimationFrame ||
				window.mozRequestAnimationFrame ||
				window.oRequestAnimationFrame ||
				window.msRequestAnimationFrame ||
				function(callback, element) {
					window.setTimeout(callback, 1000 / 60 );
				};
			})
		();
	}

	/**
 	* @author Hans Eklund, North Kingdom
 	*
 	* Modified and packaged into a module by Andrus Asumets.
 	*/

 	var toomuch = {};
	var elements = [];
	var elementId;
	var element = {}
	var index = 0;
	var animationFrame;
	var prefixedTransform;

    var settings = {
    	element: null,
        currentScale: 0.5,
        currentRotation: 0,
        containerWidth: 600,
        containerHeight: 400,
        minScale: 0.5,
        maxScale: 1.5,
        restrict: false,
        easing: 0,
        boundaries: {
            left: 0,
            right: 0,
            top: 0,
            bottom: 0
        },
        dragStartX: 0,
        dragStartY: 0,
        dragX: 0,
        dragY: 0,
        dragdDX: 0,
        dragdDY: 0,
        dragging: false,
        touchStartDistance: 0,
        touchStartAngle: 0,
        posX: 0,
        posY: 0,
        velocityX: 0,
        velocityY: 0,
        clientX: 0,
        clientY: 0
    }

	toomuch.add = function (options) {
		if('transform' in document.body.style) {
			prefixedTransform = 'transform';
		} else if('webkitTransform' in document.body.style) {
			prefixedTransform = 'webkitTransform';
		}

		var elementSettings = merge_options(settings, options);

		elementSettings.elementId = options.element.id;
		elements.push(elementSettings);
		elementId = options.element.id;

		var elementHTML = document.getElementById(elementId);
		if(window.PointerEvent) {
			element.pointers = [];

			elementHTML.addEventListener('pointerdown', pointerDownHandler, false);
		} else {
			elementHTML.addEventListener('touchstart', onTouchStart);
		}

		setElement(elementId);
		startAnimationFrame();

		return elementSettings;
	}

	toomuch.stop = function() {
		stopAnimationFrame();
		elements = [];
		elementId = null;
		element = {};
		index = 0;
	}

	toomuch.remove = function(element) {
		for (i = 0; i < elements.length; i++) {
			if (elements[i].element.id == element.id) {
				var j = 0;

				elements.splice(i, 1);

				j = i - 1;
				if (j < 0) { j = 0 };
				index = j;

				break;
			}
		}

		if (elements.length > 0) {
			elementId = elements[index].element.id;
			setElement(elementId);
		}
		else {
			stopAnimationFrame();
		}
	}

	toomuch.data = function() {
-		return elements;
-	}

	function merge_options(obj1,obj2) {
	    var obj3 = {};

	    for (var attrname in obj1) { obj3[attrname] = obj1[attrname]; }
	    for (var attrname in obj2) { obj3[attrname] = obj2[attrname]; }

	    return obj3;
	}

	function setElement(id) {
		for (i = 0; i < elements.length; i++) {
			if (elements[i].elementId === id) {
				index = i;
				element = elements[index];

				break;
			}
		}
	}

	function onTouchStart(event) {
		elementId = this.id;
		setElement(elementId);

		if(event.touches.length === 1) {
			if(!element.dragging) {
				document.addEventListener('touchmove', onTouchMove);
				document.addEventListener('touchend', onTouchEnd);
				document.addEventListener('touchcancel', onTouchEnd);

				handleDragStart(event.touches[0].clientX , event.touches[0].clientY);
			}
		} else if(event.touches.length === 2) {
			handleGestureStart(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY);
		}

		startAnimationFrame();
	}

	function onTouchMove(event) {
		event.preventDefault();

		if(event.touches.length === 1) {
			handleDragging(event.touches[0].clientX, event.touches[0].clientY);
		} else if(event.touches.length === 2) {
			handleGesture(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY);
		}
	}

	function onTouchEnd(event) {
		if(event.touches.length === 0 && element.dragging) {
			element.dragging = false;

			document.removeEventListener('touchmove', onTouchMove);
			document.removeEventListener('touchend', onTouchEnd);
			document.removeEventListener('touchcancel', onTouchEnd);
		} else if(event.touches.length === 1) {
			handleGestureStop();
			handleDragStart(event.touches[0].clientX, event.touches[0].clientY);
		}

		return true;
	}

	function indexOfPointer(pointerId){
		for (var i = 0; i < element.pointers.length; i++) {
			if(element.pointers[i].pointerId === pointerId) {
		   		return i;
			}
		}
		return -1;
	}

	function pointerDownHandler(event) {
		elementId = this.id;
		setElement(elementId);

		var pointerIndex = indexOfPointer(event.pointerId);

		if(pointerIndex < 0) {
			element.pointers.push(event);
		} else {
			element.pointers[pointerIndex] = event;
		}
		if(input.pointers.length === 1) {
			handleDragStart(element.pointers[0].clientX , element.pointers[0].clientY);

			window.addEventListener('pointermove', pointerMoveHandler, false);
			window.addEventListener('pointerup', pointerUpHandler, false);
		} else if(input.pointers.length === 2) {
			handleGestureStart(element.pointers[0].clientX, element.pointers[0].clientY, element.pointers[1].clientX, element.pointers[1].clientY);
		}
	}

	function pointerMoveHandler(event) {
		var pointerIndex = indexOfPointer(event.pointerId);

		if(pointerIndex < 0) {
			input.pointers.push(event);
		} else {
			input.pointers[pointerIndex] = event;
		}

		if(input.pointers.length === 1) {
			handleDragging(element.pointers[0].clientX, element.pointers[0].clientY);
		} else if(input.pointers.length === 2 ) {
			handleGesture(element.pointers[0].clientX, element.pointers[0].clientY, element.pointers[1].clientX, element.pointers[1].clientY);
		}
	}

	function pointerUpHandler(event) {
		element.pointers = [];

		window.removeEventListener('pointermove', pointerMoveHandler, false);
		window.removeEventListener('pointerup', pointerUpHandler, false);

		handleDragStop();
		handleGestureStop();
	}

	function handleDragStart(x, y) {
		element.dragging = true;
		element.dragX = x;
		element.dragY = y;
	}

	function handleDragging(x, y) {
		if(element.dragging) {
			element.dragdDX += x - element.dragX;
			element.dragdDY += y - element.dragY;

			element.dragX = x;
			element.dragY = y;
		}
	}

	function handleGestureStart(x1, y1, x2, y2) {
		element.isGesture = true;

		//calculate distance and angle between fingers
		var dx = x2 - x1;
		var dy = y2 - y1;

		element.touchStartDistance = Math.sqrt(dx * dx + dy * dy);
		element.touchStartAngle = Math.atan2(dy, dx);

		//we also store the current scale and rotation of the actual object we are affecting. This is needed because to enable incremental rotation/scaling.
		element.startScale = element.currentScale;
		element.startAngle = element.currentRotation;
	}

	function handleGesture(x1, y1, x2, y2) {
		if(element.isGesture) {

			//calculate distance and angle between fingers
			var dx = x2 - x1;
			var dy = y2 - y1;
			var touchDistance = Math.sqrt(dx * dx + dy * dy);
			var touchAngle = Math.atan2(dy, dx);

			//calculate the difference between current touch values and the start values
			var scalePixelChange = touchDistance - element.touchStartDistance;
			var angleChange = touchAngle - element.touchStartAngle;

			//calculate how much this should affect the actual object
			element.currentScale = element.startScale + scalePixelChange * 0.01;
			element.currentRotation = element.startAngle + (angleChange * 180 / Math.PI);

			if(element.currentScale < element.minScale) element.currentScale = element.minScale;
			if(element.currentScale > element.maxScale) element.currentScale = element.maxScale;
		}
	}

	function handleGestureStop() {
		element.isGesture = false;
	}

	function onAnimationFrame() {
		animationFrame = requestAnimationFrame(onAnimationFrame);

		if(elements.length === 0) { return };

		var elementHTML = document.getElementById(elementId);
		element = element

		if(element.dragdDX !== 0) element.velocityX = element.dragdDX;
		if(element.dragdDY !== 0) element.velocityY = element.dragdDY;

		element.posX += element.velocityX;
		element.posY += element.velocityY;

		if(element.restrict) {
			//restict horizontally
			if(element.posX < element.boundaries.left) element.posX = element.boundaries.left;
			else if(element.posX > element.boundaries.right) element.posX = element.boundaries.right;

			//restict vertically
			if(element.posY < element.boundaries.top) element.posY = element.boundaries.top;
			else if(element.posY > element.boundaries.bottom) element.posY = element.boundaries.bottom;
		}

		//set the transform
		elementHTML.style[prefixedTransform] = 'translate(' + element.posX + 'px,' + element.posY + 'px) rotate(' + element.currentRotation + 'deg) scale(' + element.currentScale + ')';

		element.velocityX = element.velocityX * element.easing;
		element.velocityY = element.velocityY * element.easing;

		element.dragdDX = 0;
		element.dragdDY = 0;

		if (typeof element.callback == 'function') {
			element.callback.call(null, element);
		}

		elements[index] = element;
	}

	function startAnimationFrame() {
	    if (!animationFrame) {
			onAnimationFrame();
	    }
	}

	function stopAnimationFrame() {
	    if (animationFrame) {
			window.cancelAnimationFrame(animationFrame);
			animationFrame = undefined;
	    }
	}

	return toomuch;
}());
<commit_msg>Fix data()<commit_after>var much = (function () {

	/**
	 * Provides requestAnimationFrame in a cross browser way.
	 * @author greggman / http://greggman.com/
	 */

	if (!window.requestAnimationFrame) {
		window.requestAnimationFrame = (function() {
			return window.webkitRequestAnimationFrame ||
				window.mozRequestAnimationFrame ||
				window.oRequestAnimationFrame ||
				window.msRequestAnimationFrame ||
				function(callback, element) {
					window.setTimeout(callback, 1000 / 60 );
				};
			})
		();
	}

	/**
 	* @author Hans Eklund, North Kingdom
 	*
 	* Modified and packaged into a module by Andrus Asumets.
 	*/

 	var toomuch = {};
	var elements = [];
	var elementId;
	var element = {}
	var index = 0;
	var animationFrame;
	var prefixedTransform;

    var settings = {
    	element: null,
        currentScale: 0.5,
        currentRotation: 0,
        containerWidth: 600,
        containerHeight: 400,
        minScale: 0.5,
        maxScale: 1.5,
        restrict: false,
        easing: 0,
        boundaries: {
            left: 0,
            right: 0,
            top: 0,
            bottom: 0
        },
        dragStartX: 0,
        dragStartY: 0,
        dragX: 0,
        dragY: 0,
        dragdDX: 0,
        dragdDY: 0,
        dragging: false,
        touchStartDistance: 0,
        touchStartAngle: 0,
        posX: 0,
        posY: 0,
        velocityX: 0,
        velocityY: 0,
        clientX: 0,
        clientY: 0
    }

	toomuch.add = function (options) {
		if('transform' in document.body.style) {
			prefixedTransform = 'transform';
		} else if('webkitTransform' in document.body.style) {
			prefixedTransform = 'webkitTransform';
		}

		var elementSettings = merge_options(settings, options);

		elementSettings.elementId = options.element.id;
		elements.push(elementSettings);
		elementId = options.element.id;

		var elementHTML = document.getElementById(elementId);
		if(window.PointerEvent) {
			element.pointers = [];

			elementHTML.addEventListener('pointerdown', pointerDownHandler, false);
		} else {
			elementHTML.addEventListener('touchstart', onTouchStart);
		}

		setElement(elementId);
		startAnimationFrame();

		return elementSettings;
	}

	toomuch.stop = function() {
		stopAnimationFrame();
		elements = [];
		elementId = null;
		element = {};
		index = 0;
	}

	toomuch.remove = function(element) {
		for (i = 0; i < elements.length; i++) {
			if (elements[i].element.id == element.id) {
				var j = 0;

				elements.splice(i, 1);

				j = i - 1;
				if (j < 0) { j = 0 };
				index = j;

				break;
			}
		}

		if (elements.length > 0) {
			elementId = elements[index].element.id;
			setElement(elementId);
		}
		else {
			stopAnimationFrame();
		}
	}

	toomuch.data = function() {
		return elements;
	}

	function merge_options(obj1,obj2) {
	    var obj3 = {};

	    for (var attrname in obj1) { obj3[attrname] = obj1[attrname]; }
	    for (var attrname in obj2) { obj3[attrname] = obj2[attrname]; }

	    return obj3;
	}

	function setElement(id) {
		for (i = 0; i < elements.length; i++) {
			if (elements[i].elementId === id) {
				index = i;
				element = elements[index];

				break;
			}
		}
	}

	function onTouchStart(event) {
		elementId = this.id;
		setElement(elementId);

		if(event.touches.length === 1) {
			if(!element.dragging) {
				document.addEventListener('touchmove', onTouchMove);
				document.addEventListener('touchend', onTouchEnd);
				document.addEventListener('touchcancel', onTouchEnd);

				handleDragStart(event.touches[0].clientX , event.touches[0].clientY);
			}
		} else if(event.touches.length === 2) {
			handleGestureStart(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY);
		}

		startAnimationFrame();
	}

	function onTouchMove(event) {
		event.preventDefault();

		if(event.touches.length === 1) {
			handleDragging(event.touches[0].clientX, event.touches[0].clientY);
		} else if(event.touches.length === 2) {
			handleGesture(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY);
		}
	}

	function onTouchEnd(event) {
		if(event.touches.length === 0 && element.dragging) {
			element.dragging = false;

			document.removeEventListener('touchmove', onTouchMove);
			document.removeEventListener('touchend', onTouchEnd);
			document.removeEventListener('touchcancel', onTouchEnd);
		} else if(event.touches.length === 1) {
			handleGestureStop();
			handleDragStart(event.touches[0].clientX, event.touches[0].clientY);
		}

		return true;
	}

	function indexOfPointer(pointerId){
		for (var i = 0; i < element.pointers.length; i++) {
			if(element.pointers[i].pointerId === pointerId) {
		   		return i;
			}
		}
		return -1;
	}

	function pointerDownHandler(event) {
		elementId = this.id;
		setElement(elementId);

		var pointerIndex = indexOfPointer(event.pointerId);

		if(pointerIndex < 0) {
			element.pointers.push(event);
		} else {
			element.pointers[pointerIndex] = event;
		}
		if(input.pointers.length === 1) {
			handleDragStart(element.pointers[0].clientX , element.pointers[0].clientY);

			window.addEventListener('pointermove', pointerMoveHandler, false);
			window.addEventListener('pointerup', pointerUpHandler, false);
		} else if(input.pointers.length === 2) {
			handleGestureStart(element.pointers[0].clientX, element.pointers[0].clientY, element.pointers[1].clientX, element.pointers[1].clientY);
		}
	}

	function pointerMoveHandler(event) {
		var pointerIndex = indexOfPointer(event.pointerId);

		if(pointerIndex < 0) {
			input.pointers.push(event);
		} else {
			input.pointers[pointerIndex] = event;
		}

		if(input.pointers.length === 1) {
			handleDragging(element.pointers[0].clientX, element.pointers[0].clientY);
		} else if(input.pointers.length === 2 ) {
			handleGesture(element.pointers[0].clientX, element.pointers[0].clientY, element.pointers[1].clientX, element.pointers[1].clientY);
		}
	}

	function pointerUpHandler(event) {
		element.pointers = [];

		window.removeEventListener('pointermove', pointerMoveHandler, false);
		window.removeEventListener('pointerup', pointerUpHandler, false);

		handleDragStop();
		handleGestureStop();
	}

	function handleDragStart(x, y) {
		element.dragging = true;
		element.dragX = x;
		element.dragY = y;
	}

	function handleDragging(x, y) {
		if(element.dragging) {
			element.dragdDX += x - element.dragX;
			element.dragdDY += y - element.dragY;

			element.dragX = x;
			element.dragY = y;
		}
	}

	function handleGestureStart(x1, y1, x2, y2) {
		element.isGesture = true;

		//calculate distance and angle between fingers
		var dx = x2 - x1;
		var dy = y2 - y1;

		element.touchStartDistance = Math.sqrt(dx * dx + dy * dy);
		element.touchStartAngle = Math.atan2(dy, dx);

		//we also store the current scale and rotation of the actual object we are affecting. This is needed because to enable incremental rotation/scaling.
		element.startScale = element.currentScale;
		element.startAngle = element.currentRotation;
	}

	function handleGesture(x1, y1, x2, y2) {
		if(element.isGesture) {

			//calculate distance and angle between fingers
			var dx = x2 - x1;
			var dy = y2 - y1;
			var touchDistance = Math.sqrt(dx * dx + dy * dy);
			var touchAngle = Math.atan2(dy, dx);

			//calculate the difference between current touch values and the start values
			var scalePixelChange = touchDistance - element.touchStartDistance;
			var angleChange = touchAngle - element.touchStartAngle;

			//calculate how much this should affect the actual object
			element.currentScale = element.startScale + scalePixelChange * 0.01;
			element.currentRotation = element.startAngle + (angleChange * 180 / Math.PI);

			if(element.currentScale < element.minScale) element.currentScale = element.minScale;
			if(element.currentScale > element.maxScale) element.currentScale = element.maxScale;
		}
	}

	function handleGestureStop() {
		element.isGesture = false;
	}

	function onAnimationFrame() {
		animationFrame = requestAnimationFrame(onAnimationFrame);

		if(elements.length === 0) { return };

		var elementHTML = document.getElementById(elementId);
		element = element

		if(element.dragdDX !== 0) element.velocityX = element.dragdDX;
		if(element.dragdDY !== 0) element.velocityY = element.dragdDY;

		element.posX += element.velocityX;
		element.posY += element.velocityY;

		if(element.restrict) {
			//restict horizontally
			if(element.posX < element.boundaries.left) element.posX = element.boundaries.left;
			else if(element.posX > element.boundaries.right) element.posX = element.boundaries.right;

			//restict vertically
			if(element.posY < element.boundaries.top) element.posY = element.boundaries.top;
			else if(element.posY > element.boundaries.bottom) element.posY = element.boundaries.bottom;
		}

		//set the transform
		elementHTML.style[prefixedTransform] = 'translate(' + element.posX + 'px,' + element.posY + 'px) rotate(' + element.currentRotation + 'deg) scale(' + element.currentScale + ')';

		element.velocityX = element.velocityX * element.easing;
		element.velocityY = element.velocityY * element.easing;

		element.dragdDX = 0;
		element.dragdDY = 0;

		if (typeof element.callback == 'function') {
			element.callback.call(null, element);
		}

		elements[index] = element;
	}

	function startAnimationFrame() {
	    if (!animationFrame) {
			onAnimationFrame();
	    }
	}

	function stopAnimationFrame() {
	    if (animationFrame) {
			window.cancelAnimationFrame(animationFrame);
			animationFrame = undefined;
	    }
	}

	return toomuch;
}());
<|endoftext|>


<commit_before>const twgl = require('twgl.js');

const Skin = require('./Skin');
const SvgRenderer = require('scratch-svg-renderer').SVGRenderer;

const MAX_TEXTURE_DIMENSION = 2048;

/**
 * All scaled renderings of the SVG are stored in an array. The 1.0 scale of
 * the SVG is stored at the 8th index. The smallest possible 1 / 256 scale
 * rendering is stored at the 0th index.
 * @const {number}
 */
const INDEX_OFFSET = 8;

class SVGSkin extends Skin {
    /**
     * Create a new SVG skin.
     * @param {!int} id - The ID for this Skin.
     * @param {!RenderWebGL} renderer - The renderer which will use this skin.
     * @constructor
     * @extends Skin
     */
    constructor (id, renderer) {
        super(id);

        /** @type {RenderWebGL} */
        this._renderer = renderer;

        /** @type {SvgRenderer} */
        this._svgRenderer = new SvgRenderer();

        /** @type {Array<WebGLTexture>} */
        this._scaledMIPs = [];

        /** @type {number} */
        this._largestMIPScale = 0;

        /**
        * Ratio of the size of the SVG and the max size of the WebGL texture
        * @type {Number}
        */
        this._maxTextureScale = 1;
    }

    /**
     * Dispose of this object. Do not use it after calling this method.
     */
    dispose () {
        this.resetMIPs();
        super.dispose();
    }

    /**
     * @return {Array<number>} the natural size, in Scratch units, of this skin.
     */
    get size () {
        return this._svgRenderer.size;
    }

    /**
     * Set the origin, in object space, about which this Skin should rotate.
     * @param {number} x - The x coordinate of the new rotation center.
     * @param {number} y - The y coordinate of the new rotation center.
     */
    setRotationCenter (x, y) {
        const viewOffset = this._svgRenderer.viewOffset;
        super.setRotationCenter(x - viewOffset[0], y - viewOffset[1]);
    }

    /**
     * Create a MIP for a given scale.
     * @param {number} scale - The relative size of the MIP
     * @return {SVGMIP} An object that handles creating and updating SVG textures.
     */
    createMIP (scale) {
        this._svgRenderer.draw(scale);

        // Pull out the ImageData from the canvas. ImageData speeds up
        // updating Silhouette and is better handled by more browsers in
        // regards to memory.
        const canvas = this._svgRenderer.canvas;
        // If one of the canvas dimensions is 0, set this MIP to an empty image texture.
        // This avoids an IndexSizeError from attempting to getImageData when one of the dimensions is 0.
        if (canvas.width === 0 || canvas.height === 0) return super.getTexture();

        const context = canvas.getContext('2d');
        const textureData = context.getImageData(0, 0, canvas.width, canvas.height);

        const textureOptions = {
            auto: false,
            wrap: this._renderer.gl.CLAMP_TO_EDGE,
            src: textureData,
            premultiplyAlpha: true
        };

        const mip = twgl.createTexture(this._renderer.gl, textureOptions);

        // Check if this is the largest MIP created so far. Currently, silhouettes only get scaled up.
        if (this._largestMIPScale < scale) {
            this._silhouette.update(textureData);
            this._largestMIPScale = scale;
        }

        return mip;
    }

    updateSilhouette (scale = [100, 100]) {
        // Ensure a silhouette exists.
        this.getTexture(scale);
    }

    /**
     * @param {Array<number>} scale - The scaling factors to be used, each in the [0,100] range.
     * @return {WebGLTexture} The GL texture representation of this skin when drawing at the given scale.
     */
    getTexture (scale) {
        // The texture only ever gets uniform scale. Take the larger of the two axes.
        const scaleMax = scale ? Math.max(Math.abs(scale[0]), Math.abs(scale[1])) : 100;
        const requestedScale = Math.min(scaleMax / 100, this._maxTextureScale);

        const mipLevel = Math.max(Math.round(Math.log2(requestedScale)) + INDEX_OFFSET, 0);
        // Can't use bitwise stuff here because we need to handle negative exponents
        const mipScale = Math.pow(2, mipLevel - INDEX_OFFSET);

        if (this._svgRenderer.loaded && !this._scaledMIPs[mipLevel]) {
            this._scaledMIPs[mipLevel] = this.createMIP(mipScale);
        }

        return this._scaledMIPs[mipLevel] || super.getTexture();
    }

    /**
     * Do a hard reset of the existing MIPs by deleting them.
     * @param {Array<number>} [rotationCenter] - Optional rotation center for the SVG. If not supplied, it will be
     * calculated from the bounding box
     * @fires Skin.event:WasAltered
     */
    resetMIPs () {
        this._scaledMIPs.forEach(oldMIP => this._renderer.gl.deleteTexture(oldMIP));
        this._scaledMIPs.length = 0;
        this._largestMIPScale = 0;
    }

    /**
     * Set the contents of this skin to a snapshot of the provided SVG data.
     * @param {string} svgData - new SVG to use.
     * @param {Array<number>} [rotationCenter] - Optional rotation center for the SVG.
     */
    setSVG (svgData, rotationCenter) {
        this._svgRenderer.loadSVG(svgData, false, () => {
            const svgSize = this._svgRenderer.size;
            if (svgSize[0] === 0 || svgSize[1] === 0) {
                super.setEmptyImageData();
                return;
            }

            const maxDimension = Math.ceil(Math.max(this.size[0], this.size[1]));
            let testScale = 2;
            for (testScale; maxDimension * testScale <= MAX_TEXTURE_DIMENSION; testScale *= 2) {
                this._maxTextureScale = testScale;
            }

            this.resetMIPs();

            if (typeof rotationCenter === 'undefined') rotationCenter = this.calculateRotationCenter();
            this.setRotationCenter.apply(this, rotationCenter);
            this.emit(Skin.Events.WasAltered);
        });
    }

}

module.exports = SVGSkin;
<commit_msg>Don't blur vector skins<commit_after>const twgl = require('twgl.js');

const Skin = require('./Skin');
const SvgRenderer = require('scratch-svg-renderer').SVGRenderer;

const MAX_TEXTURE_DIMENSION = 2048;

/**
 * All scaled renderings of the SVG are stored in an array. The 1.0 scale of
 * the SVG is stored at the 8th index. The smallest possible 1 / 256 scale
 * rendering is stored at the 0th index.
 * @const {number}
 */
const INDEX_OFFSET = 8;

class SVGSkin extends Skin {
    /**
     * Create a new SVG skin.
     * @param {!int} id - The ID for this Skin.
     * @param {!RenderWebGL} renderer - The renderer which will use this skin.
     * @constructor
     * @extends Skin
     */
    constructor (id, renderer) {
        super(id);

        /** @type {RenderWebGL} */
        this._renderer = renderer;

        /** @type {SvgRenderer} */
        this._svgRenderer = new SvgRenderer();

        /** @type {Array<WebGLTexture>} */
        this._scaledMIPs = [];

        /** @type {number} */
        this._largestMIPScale = 0;

        /**
        * Ratio of the size of the SVG and the max size of the WebGL texture
        * @type {Number}
        */
        this._maxTextureScale = 1;
    }

    /**
     * Dispose of this object. Do not use it after calling this method.
     */
    dispose () {
        this.resetMIPs();
        super.dispose();
    }

    /**
     * @return {Array<number>} the natural size, in Scratch units, of this skin.
     */
    get size () {
        return this._svgRenderer.size;
    }

    /**
     * Set the origin, in object space, about which this Skin should rotate.
     * @param {number} x - The x coordinate of the new rotation center.
     * @param {number} y - The y coordinate of the new rotation center.
     */
    setRotationCenter (x, y) {
        const viewOffset = this._svgRenderer.viewOffset;
        super.setRotationCenter(x - viewOffset[0], y - viewOffset[1]);
    }

    /**
     * Create a MIP for a given scale.
     * @param {number} scale - The relative size of the MIP
     * @return {SVGMIP} An object that handles creating and updating SVG textures.
     */
    createMIP (scale) {
        this._svgRenderer.draw(scale);

        // Pull out the ImageData from the canvas. ImageData speeds up
        // updating Silhouette and is better handled by more browsers in
        // regards to memory.
        const canvas = this._svgRenderer.canvas;
        // If one of the canvas dimensions is 0, set this MIP to an empty image texture.
        // This avoids an IndexSizeError from attempting to getImageData when one of the dimensions is 0.
        if (canvas.width === 0 || canvas.height === 0) return super.getTexture();

        const context = canvas.getContext('2d');
        const textureData = context.getImageData(0, 0, canvas.width, canvas.height);

        const textureOptions = {
            auto: false,
            wrap: this._renderer.gl.CLAMP_TO_EDGE,
            src: textureData,
            premultiplyAlpha: true
        };

        const mip = twgl.createTexture(this._renderer.gl, textureOptions);

        // Check if this is the largest MIP created so far. Currently, silhouettes only get scaled up.
        if (this._largestMIPScale < scale) {
            this._silhouette.update(textureData);
            this._largestMIPScale = scale;
        }

        return mip;
    }

    updateSilhouette (scale = [100, 100]) {
        // Ensure a silhouette exists.
        this.getTexture(scale);
    }

    /**
     * @param {Array<number>} scale - The scaling factors to be used, each in the [0,100] range.
     * @return {WebGLTexture} The GL texture representation of this skin when drawing at the given scale.
     */
    getTexture (scale) {
        // The texture only ever gets uniform scale. Take the larger of the two axes.
        const scaleMax = scale ? Math.max(Math.abs(scale[0]), Math.abs(scale[1])) : 100;
        const requestedScale = Math.min(scaleMax / 100, this._maxTextureScale);

        // Math.ceil(Math.log2(scale)) means we use the "1x" texture at (0.5, 1] scale,
        // the "2x" texture at (1, 2] scale, the "4x" texture at (2, 4] scale, etc.
        // This means that one texture pixel will always be between 0.5x and 1x the size of one rendered pixel,
        // but never bigger than one rendered pixel--this prevents blurriness from blowing up the texture too much.
        const mipLevel = Math.max(Math.ceil(Math.log2(requestedScale)) + INDEX_OFFSET, 0);
        // Can't use bitwise stuff here because we need to handle negative exponents
        const mipScale = Math.pow(2, mipLevel - INDEX_OFFSET);

        if (this._svgRenderer.loaded && !this._scaledMIPs[mipLevel]) {
            this._scaledMIPs[mipLevel] = this.createMIP(mipScale);
        }

        return this._scaledMIPs[mipLevel] || super.getTexture();
    }

    /**
     * Do a hard reset of the existing MIPs by deleting them.
     * @param {Array<number>} [rotationCenter] - Optional rotation center for the SVG. If not supplied, it will be
     * calculated from the bounding box
     * @fires Skin.event:WasAltered
     */
    resetMIPs () {
        this._scaledMIPs.forEach(oldMIP => this._renderer.gl.deleteTexture(oldMIP));
        this._scaledMIPs.length = 0;
        this._largestMIPScale = 0;
    }

    /**
     * Set the contents of this skin to a snapshot of the provided SVG data.
     * @param {string} svgData - new SVG to use.
     * @param {Array<number>} [rotationCenter] - Optional rotation center for the SVG.
     */
    setSVG (svgData, rotationCenter) {
        this._svgRenderer.loadSVG(svgData, false, () => {
            const svgSize = this._svgRenderer.size;
            if (svgSize[0] === 0 || svgSize[1] === 0) {
                super.setEmptyImageData();
                return;
            }

            const maxDimension = Math.ceil(Math.max(this.size[0], this.size[1]));
            let testScale = 2;
            for (testScale; maxDimension * testScale <= MAX_TEXTURE_DIMENSION; testScale *= 2) {
                this._maxTextureScale = testScale;
            }

            this.resetMIPs();

            if (typeof rotationCenter === 'undefined') rotationCenter = this.calculateRotationCenter();
            this.setRotationCenter.apply(this, rotationCenter);
            this.emit(Skin.Events.WasAltered);
        });
    }

}

module.exports = SVGSkin;
<|endoftext|>