UNPKG

@cocalc/project

Version:
320 lines (292 loc) 8.96 kB
/* * 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(); }