smc-hub
Version:
CoCalc: Backend webserver component
583 lines (581 loc) • 27 kB
JavaScript
"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