UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

583 lines (581 loc) 27 kB
"use strict"; /* * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. * License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details */ var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Changes = void 0; /* The Changes class is a useful building block for making changefeeds. It lets you watch when given columns change in a given table, and be notified when a where condition is satisfied. IMPORTANT: If an error event is emitted then Changes object will close and not work any further! You must recreate it. */ var events_1 = require("events"); var misc = __importStar(require("smc-util/misc")); var awaiting_1 = require("awaiting"); var changefeed_query_1 = require("./changefeed-query"); function parse_action(obj) { var s = "" + obj.toLowerCase(); if (s === "delete" || s === "insert" || s === "update") { return s; } throw Error("invalid action \"" + s + "\""); } var Changes = /** @class */ (function (_super) { __extends(Changes, _super); function Changes(db, table, select, watch, where, cb) { var _this = _super.call(this) || this; _this.val_update_cache = {}; _this.handle_change = _this.handle_change.bind(_this); _this.db = db; _this.table = table; _this.select = select; _this.watch = watch; _this.where = where; _this.init(cb); return _this; } Changes.prototype.init = function (cb) { return __awaiter(this, void 0, void 0, function () { var _a, err_1; return __generator(this, function (_b) { switch (_b.label) { case 0: this.dbg("constructor")("select=" + misc.to_json(this.select) + ", watch=" + misc.to_json(this.watch) + ", @_where=" + misc.to_json(this.where)); try { this.init_where(); } catch (e) { cb("error initializing where conditions -- " + e); return [2 /*return*/]; } _b.label = 1; case 1: _b.trys.push([1, 3, , 4]); _a = this; return [4 /*yield*/, awaiting_1.callback(this.db._listen, this.table, this.select, this.watch)]; case 2: _a.trigger_name = _b.sent(); return [3 /*break*/, 4]; case 3: err_1 = _b.sent(); cb(err_1); return [2 /*return*/]; case 4: this.db.on(this.trigger_name, this.handle_change); // NOTE: we close on *connect*, not on disconnect, since then clients // that try to reconnect will only try to do so when we have an actual // connection to the database. No point in worrying them while trying // to reconnect, which only makes matters worse (as they panic and // requests pile up!). this.db.once("connect", this.close); cb(undefined, this); return [2 /*return*/]; } }); }); }; Changes.prototype.dbg = function (f) { return this.db._dbg("Changes(table='" + this.table + "')." + f); }; // this breaks the changefeed -- client must recreate it; nothing further will work at all. Changes.prototype.fail = function (err) { if (this.closed) { return; } this.dbg("_fail")("err='" + err + "'"); this.emit("error", new Error(err)); this.close(); }; Changes.prototype.close = function () { if (this.closed) { return; } this.emit("close", { action: "close" }); this.removeAllListeners(); if (this.db != null) { this.db.removeListener(this.trigger_name, this.handle_change); this.db.removeListener("connect", this.close); this.db._stop_listening(this.table, this.select, this.watch); } misc.close(this); this.closed = true; }; Changes.prototype.insert = function (where) { return __awaiter(this, void 0, void 0, function () { var where0, k, v, results, err_2, results_1, results_1_1, x, change; var e_1, _a; return __generator(this, function (_b) { switch (_b.label) { case 0: where0 = {}; for (k in where) { v = where[k]; where0[k + " = $"] = v; } _b.label = 1; case 1: _b.trys.push([1, 3, , 4]); return [4 /*yield*/, changefeed_query_1.query({ db: this.db, select: this.watch.concat(misc.keys(this.select)), table: this.table, where: where0, one: false, })]; case 2: results = _b.sent(); return [3 /*break*/, 4]; case 3: err_2 = _b.sent(); this.fail(err_2); // this is game over return [2 /*return*/]; case 4: try { for (results_1 = __values(results), results_1_1 = results_1.next(); !results_1_1.done; results_1_1 = results_1.next()) { x = results_1_1.value; if (this.match_condition(x)) { misc.map_mutate_out_undefined(x); change = { action: "insert", new_val: x }; this.emit("change", change); } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (results_1_1 && !results_1_1.done && (_a = results_1.return)) _a.call(results_1); } finally { if (e_1) throw e_1.error; } } return [2 /*return*/]; } }); }); }; Changes.prototype.delete = function (where) { // listener is meant to delete everything that *matches* the where, so // there is no need to actually do a query. var change = { action: "delete", old_val: where }; this.emit("change", change); }; Changes.prototype.handle_change = function (mesg) { return __awaiter(this, void 0, void 0, function () { var k, r, v, action, where, result, err_3, key, this_val, new_val, x; return __generator(this, function (_a) { switch (_a.label) { case 0: if (this.closed) { return [2 /*return*/]; } // this.dbg("handle_change")(JSON.stringify(mesg)); if (mesg[0] === "DELETE") { if (!this.match_condition(mesg[2])) { return [2 /*return*/]; } this.emit("change", { action: "delete", old_val: mesg[2] }); return [2 /*return*/]; } if (typeof mesg[0] !== "string") { throw Error("invalid mesg -- mesg[0] must be a string"); } action = parse_action(mesg[0]); if (!this.match_condition(mesg[1])) { // object does not match condition if (action !== "update") { // new object that doesn't match condition -- nothing to do. return [2 /*return*/]; } // fill in for each part that we watch in new object the same // data in the old object, in case it is missing. // TODO: when is this actually needed? for (k in mesg[1]) { v = mesg[1][k]; if (mesg[2][k] == null) { mesg[2][k] = v; } } if (this.match_condition(mesg[2])) { // the old object was in our changefeed, but the UPDATE made it not // anymore, so we emit delete action. this.emit("change", { action: "delete", old_val: mesg[2] }); } // Nothing more to do. return [2 /*return*/]; } if (this.watch.length === 0) { // No additional columns are being watched at all -- we only // care about what's in the mesg. r = { action: action, new_val: mesg[1] }; this.emit("change", r); return [2 /*return*/]; } where = {}; for (k in mesg[1]) { v = mesg[1][k]; where[k + " = $"] = v; } _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, changefeed_query_1.query({ db: this.db, select: this.watch, table: this.table, where: where, one: true, })]; case 2: result = _a.sent(); return [3 /*break*/, 4]; case 3: err_3 = _a.sent(); this.fail(err_3); return [2 /*return*/]; case 4: // we do know from stacktraces that new_val_update is called after closed // this must have happened during waiting on the query. aborting early. if (this.closed) { return [2 /*return*/]; } if (result == null) { // This happens when record isn't deleted, but some // update results in the object being removed from our // selection criterion... which we view as "delete". this.emit("change", { action: "delete", old_val: mesg[1] }); return [2 /*return*/]; } key = JSON.stringify(mesg[1]); this_val = misc.merge(result, mesg[1]); if (action == "update") { x = this.new_val_update(mesg[1], this_val, key); if (x == null) { // happens if this.closed is true -- double check for safety & TS happyness. return [2 /*return*/]; } action = x.action; // may be insert in case no previous cached info. new_val = x.new_val; } else { // not update and not delete (could have been a delete and write // before we did above query, so treat as insert). action = "insert"; new_val = this_val; } this.val_update_cache[key] = this_val; r = { action: action, new_val: new_val }; this.emit("change", r); return [2 /*return*/]; } }); }); }; Changes.prototype.new_val_update = function (primary_part, this_val, key) { if (this.closed) { return; } var prev_val = this.val_update_cache[key]; if (prev_val == null) { return { new_val: this_val, action: "insert" }; // not enough info to make a diff } // Send only the fields that changed between // prev_val and this_val, along with the primary part. var new_val = misc.copy(primary_part); // Not using lodash isEqual below, since we want equal Date objects // to compare as equal. If JSON is randomly re-ordered, that's fine since // it is just slightly less efficienct. for (var field in this_val) { if (new_val[field] === undefined && JSON.stringify(this_val[field]) != JSON.stringify(prev_val[field])) { new_val[field] = this_val[field]; } } return { new_val: new_val, action: "update" }; }; Changes.prototype.init_where = function () { var e_2, _a, e_3, _b, e_4, _c; var _this = this; if (typeof this.where === "function") { // user provided function this.match_condition = this.where; return; } var w; if (misc.is_object(this.where)) { w = [this.where]; } else { // TODO: misc.is_object needs to be a typescript checker instead, so // this as isn't needed. w = this.where; } this.condition = {}; var add_condition = function (field, op, val) { if (_this.condition == null) return; // won't happen var f, g; field = field.trim(); if (field[0] === '"') { // de-quote field = field.slice(1, field.length - 1); } if (_this.select[field] == null) { throw Error("'" + field + "' must be in select=\"" + JSON.stringify(_this.select) + "\""); } if (misc.is_object(val)) { throw Error("val (=" + misc.to_json(val) + ") must not be an object"); } if (misc.is_array(val)) { if (op === "=" || op === "==") { // containment f = function (x) { var e_5, _a; try { for (var val_1 = __values(val), val_1_1 = val_1.next(); !val_1_1.done; val_1_1 = val_1.next()) { var v = val_1_1.value; if (x === v) { return true; } } } catch (e_5_1) { e_5 = { error: e_5_1 }; } finally { try { if (val_1_1 && !val_1_1.done && (_a = val_1.return)) _a.call(val_1); } finally { if (e_5) throw e_5.error; } } return false; }; } else if (op === "!=" || op === "<>") { // not contained in f = function (x) { var e_6, _a; try { for (var val_2 = __values(val), val_2_1 = val_2.next(); !val_2_1.done; val_2_1 = val_2.next()) { var v = val_2_1.value; if (x === v) { return false; } } } catch (e_6_1) { e_6 = { error: e_6_1 }; } finally { try { if (val_2_1 && !val_2_1.done && (_a = val_2.return)) _a.call(val_2); } finally { if (e_6) throw e_6.error; } } return true; }; } else { throw Error("if val is an array, then op must be = or !="); } } else if (misc.is_date(val)) { // Inputs to condition come back as JSON, which doesn't know // about timestamps, so we convert them to date objects. if (op == "=" || op == "==") { f = function (x) { return new Date(x).valueOf() - val === 0; }; } else if (op == "!=" || op == "<>") { f = function (x) { return new Date(x).valueOf() - val !== 0; }; } else { g = misc.op_to_function(op); f = function (x) { return g(new Date(x), val); }; } } else { g = misc.op_to_function(op); f = function (x) { return g(x, val); }; } _this.condition[field] = f; }; try { for (var w_1 = __values(w), w_1_1 = w_1.next(); !w_1_1.done; w_1_1 = w_1.next()) { var obj = w_1_1.value; var found = void 0, i = void 0, op = void 0; if (misc.is_object(obj)) { for (var k in obj) { var val = obj[k]; /* k should be of one of the following forms - "field op $::TYPE" - "field op $" or - "field op any($)" - 'field' (defaults to =) where op is one of =, <, >, <=, >=, != val must be: - something where javascript === and comparisons works as you expect! - or an array, in which case op must be = or !=, and we ALWAYS do inclusion (analogue of any). */ found = false; try { for (var _d = (e_3 = void 0, __values(misc.operators)), _e = _d.next(); !_e.done; _e = _d.next()) { op = _e.value; i = k.indexOf(op); if (i !== -1) { add_condition(k.slice(0, i).trim(), op, val); found = true; break; } } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (_e && !_e.done && (_b = _d.return)) _b.call(_d); } finally { if (e_3) throw e_3.error; } } if (!found) { throw Error("unable to parse '" + k + "'"); } } } else if (typeof obj === "string") { found = false; try { for (var _f = (e_4 = void 0, __values(misc.operators)), _g = _f.next(); !_g.done; _g = _f.next()) { op = _g.value; i = obj.indexOf(op); if (i !== -1) { add_condition(obj.slice(0, i), op, eval(obj.slice(i + op.length).trim())); found = true; break; } } } catch (e_4_1) { e_4 = { error: e_4_1 }; } finally { try { if (_g && !_g.done && (_c = _f.return)) _c.call(_f); } finally { if (e_4) throw e_4.error; } } if (!found) { throw Error("unable to parse '" + obj + "'"); } } else { throw Error("NotImplementedError"); } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (w_1_1 && !w_1_1.done && (_a = w_1.return)) _a.call(w_1); } finally { if (e_2) throw e_2.error; } } if (misc.len(this.condition) === 0) { delete this.condition; } this.match_condition = function (obj) { //console.log '_match_condition', obj if (_this.condition == null) { return true; } for (var field in _this.condition) { var f = _this.condition[field]; if (!f(obj[field])) { //console.log 'failed due to field ', field return false; } } return true; }; }; return Changes; }(events_1.EventEmitter)); exports.Changes = Changes; //# sourceMappingURL=changefeed.js.map