@cocalc/project
Version:
CoCalc: project daemon
275 lines • 10.5 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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (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;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.lean_server = exports.Lean = void 0;
const underscore_1 = require("underscore");
const lean_client = __importStar(require("lean-client-js-node"));
const awaiting_1 = require("awaiting");
const async_utils_1 = require("@cocalc/util/async-utils");
const misc_1 = require("@cocalc/util/misc");
const hof_1 = require("async-await-utils/hof");
const events_1 = require("events");
// do not try to sync with lean more frequently than this
// unless it is completing quickly.
const SYNC_INTERVAL = 6000;
class Lean extends events_1.EventEmitter {
constructor(client) {
super();
this.paths = {};
this._state = { tasks: [], paths: {} };
this.running = {};
this.server = (0, hof_1.reuseInFlight)(this.server);
this.client = client;
this.dbg = this.client.dbg("LEAN SERVER");
this.running = {};
}
close() {
this.kill();
(0, misc_1.close)(this);
}
is_running(path) {
return !!this.running[path] && now() - this.running[path] < SYNC_INTERVAL;
}
// nothing actually async in here... yet.
async server() {
if (this._server != undefined) {
if (this._server.alive()) {
return this._server;
}
// Kill cleans up any assumptions about stuff
// being sync'd.
this.kill();
// New server will now be created... below.
}
this._server = new lean_client.Server(new lean_client.ProcessTransport("lean", process.env.HOME ? process.env.HOME : ".", // satisfy typescript.
["-M 4096"]));
this._server.error.on((err) => this.dbg("error:", err));
this._server.allMessages.on((allMessages) => {
this.dbg("messages: ", allMessages);
const new_messages = {};
for (const x of allMessages.msgs) {
const path = x.file_name;
delete x.file_name;
if (new_messages[path] === undefined) {
new_messages[path] = [x];
}
else {
new_messages[path].push(x);
}
}
for (const path in this._state.paths) {
this.dbg("messages for ", path, new_messages[path]);
if (new_messages[path] === undefined) {
new_messages[path] = [];
}
this.dbg("messages for ", path, new_messages[path], this._state.paths[path]);
// length 0 is a special case needed when going from pos number of messages to none.
if (new_messages[path].length === 0 ||
!(0, underscore_1.isEqual)(this._state.paths[path], new_messages[path])) {
this.dbg("messages for ", path, "EMIT!");
this.emit("messages", path, new_messages[path]);
this._state.paths[path] = new_messages[path];
}
}
});
this._server.tasks.on((currentTasks) => {
const { tasks } = currentTasks;
this.dbg("tasks: ", tasks);
const running = {};
for (const task of tasks) {
running[task.file_name] = true;
}
for (const path in running) {
const v = [];
for (const task of tasks) {
if (task.file_name === path) {
delete task.file_name; // no longer needed
v.push(task);
}
}
this.emit("tasks", path, v);
}
for (const path in this.running) {
if (!running[path]) {
this.dbg("server", path, " done; no longer running");
this.running[path] = 0;
this.emit("tasks", path, []);
if (this.paths[path].changed) {
// file changed while lean was running -- so run lean again.
this.dbg("server", path, " changed while running, so running again");
this.paths[path].on_change();
}
}
}
});
this._server.connect();
return this._server;
}
// Start learn server parsing and reporting info about the given file.
// It will get updated whenever the file change.
async register(path) {
this.dbg("register", path);
if (this.paths[path] !== undefined) {
this.dbg("register", path, "already registered");
return;
}
// get the syncstring and start updating based on content
let syncstring = undefined;
while (syncstring == null) {
// todo change to be event driven!
syncstring = this.client.syncdoc({ path });
if (syncstring == null) {
await (0, awaiting_1.delay)(1000);
}
else if (syncstring.get_state() != "ready") {
await (0, async_utils_1.once)(syncstring, "ready");
}
}
const on_change = async () => {
this.dbg("sync", path);
if (syncstring._closed) {
this.dbg("sync", path, "closed");
return;
}
if (this.is_running(path)) {
// already running, so do nothing - it will rerun again when done with current run.
this.dbg("sync", path, "already running");
this.paths[path].changed = true;
return;
}
const value = syncstring.to_str();
if (this.paths[path].last_value === value) {
this.dbg("sync", path, "skipping sync since value did not change");
return;
}
if (value.trim() === "") {
this.dbg("sync", path, "skipping sync document is empty (and LEAN behaves weird in this case)");
this.emit("sync", path, syncstring.hash_of_live_version());
return;
}
this.paths[path].last_value = value;
this._state.paths[path] = [];
this.running[path] = now();
this.paths[path].changed = false;
this.dbg("sync", path, "causing server sync now");
await (await this.server()).sync(path, value);
this.emit("sync", path, syncstring.hash_of_live_version());
};
this.paths[path] = {
syncstring,
on_change,
};
syncstring.on("change", on_change);
if (!syncstring._closed) {
on_change();
}
syncstring.on("closed", () => {
this.unregister(path);
});
}
// Stop updating given file on changes.
unregister(path) {
this.dbg("unregister", path);
if (!this.paths[path]) {
// not watching it
return;
}
const x = this.paths[path];
delete this.paths[path];
delete this.running[path];
x.syncstring.removeListener("change", x.on_change);
x.syncstring.close();
}
// Kill the lean server and unregister all paths.
kill() {
this.dbg("kill");
if (this._server != undefined) {
for (const path in this.paths) {
this.unregister(path);
}
this._server.dispose();
delete this._server;
}
}
async restart() {
this.dbg("restart");
if (this._server != undefined) {
for (const path in this.paths) {
this.unregister(path);
}
await this._server.restart();
}
}
async info(path, line, column) {
this.dbg("info", path, line, column);
if (!this.paths[path]) {
this.register(path);
await (0, awaiting_1.callback)((cb) => this.once(`sync-#{path}`, cb));
}
return await (await this.server()).info(path, line, column);
}
async complete(path, line, column, skipCompletions) {
this.dbg("complete", path, line, column);
if (!this.paths[path]) {
this.register(path);
await (0, awaiting_1.callback)((cb) => this.once(`sync-#{path}`, cb));
}
const resp = await (await this.server()).complete(path, line, column, skipCompletions);
//this.dbg("complete response", path, line, column, resp);
return resp;
}
async version() {
return (await this.server()).getVersion();
}
// Return state of parsing for everything that is currently registered.
state() {
return this._state;
}
messages(path) {
const x = this._state.paths[path];
if (x !== undefined) {
return x;
}
return [];
}
}
exports.Lean = Lean;
let singleton;
// Return the singleton lean instance. The client is assumed to never change.
function lean_server(client) {
if (singleton === undefined) {
singleton = new Lean(client);
}
return singleton;
}
exports.lean_server = lean_server;
function now() {
return new Date().valueOf();
}
//# sourceMappingURL=lean.js.map