@cocalc/project
Version:
CoCalc: project daemon
320 lines (292 loc) • 8.96 kB
text/typescript
/*
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
*/
import { isEqual } from "underscore";
import * as lean_client from "lean-client-js-node";
import { callback, delay } from "awaiting";
import { once } from "@cocalc/util/async-utils";
import { close } from "@cocalc/util/misc";
import { reuseInFlight } from "async-await-utils/hof";
import { EventEmitter } from "events";
type SyncString = any;
type Client = any;
type LeanServer = any;
// do not try to sync with lean more frequently than this
// unless it is completing quickly.
const SYNC_INTERVAL: number = 6000;
// What lean has told us about a given file.
type Message = any;
type Messages = Message[];
// What lean has told us about all files.
interface State {
tasks: any;
paths: { [key: string]: Messages };
}
export class Lean extends EventEmitter {
paths: { [key: string]: SyncString } = {};
client: Client;
_server: LeanServer | undefined;
_state: State = { tasks: [], paths: {} };
private running: { [key: string]: number } = {};
dbg: Function;
constructor(client: Client) {
super();
this.server = reuseInFlight(this.server);
this.client = client;
this.dbg = this.client.dbg("LEAN SERVER");
this.running = {};
}
close(): void {
this.kill();
close(this);
}
is_running(path: string): boolean {
return !!this.running[path] && now() - this.running[path] < SYNC_INTERVAL;
}
// nothing actually async in here... yet.
private async server(): Promise<LeanServer> {
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: string = 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 ||
!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: any[] = [];
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: string): Promise<void> {
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: any = undefined;
while (syncstring == null) {
// todo change to be event driven!
syncstring = this.client.syncdoc({ path });
if (syncstring == null) {
await delay(1000);
} else if (syncstring.get_state() != "ready") {
await 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: string = 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: string): void {
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(): void {
this.dbg("kill");
if (this._server != undefined) {
for (const path in this.paths) {
this.unregister(path);
}
this._server.dispose();
delete this._server;
}
}
async restart(): Promise<void> {
this.dbg("restart");
if (this._server != undefined) {
for (const path in this.paths) {
this.unregister(path);
}
await this._server.restart();
}
}
async info(
path: string,
line: number,
column: number
): Promise<lean_client.InfoResponse> {
this.dbg("info", path, line, column);
if (!this.paths[path]) {
this.register(path);
await callback((cb) => this.once(`sync-#{path}`, cb));
}
return await (await this.server()).info(path, line, column);
}
async complete(
path: string,
line: number,
column: number,
skipCompletions?: boolean
): Promise<lean_client.CompleteResponse> {
this.dbg("complete", path, line, column);
if (!this.paths[path]) {
this.register(path);
await 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(): Promise<string> {
return (await this.server()).getVersion();
}
// Return state of parsing for everything that is currently registered.
state(): State {
return this._state;
}
messages(path: string): any[] {
const x = this._state.paths[path];
if (x !== undefined) {
return x;
}
return [];
}
}
let singleton: Lean | undefined;
// Return the singleton lean instance. The client is assumed to never change.
export function lean_server(client: Client): Lean {
if (singleton === undefined) {
singleton = new Lean(client);
}
return singleton;
}
function now(): number {
return new Date().valueOf();
}