CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/project/lean/lean.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { callback, delay } from "awaiting";
7
import * as lean_client from "lean-client-js-node";
8
import { isEqual } from "lodash";
9
import { EventEmitter } from "node:events";
10
11
import type { Client } from "@cocalc/project/client";
12
import { once } from "@cocalc/util/async-utils";
13
import { close } from "@cocalc/util/misc";
14
import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
15
16
type SyncString = any;
17
type LeanServer = any;
18
19
// do not try to sync with lean more frequently than this
20
// unless it is completing quickly.
21
const SYNC_INTERVAL: number = 6000;
22
23
// What lean has told us about a given file.
24
type Message = any;
25
type Messages = Message[];
26
27
// What lean has told us about all files.
28
interface State {
29
tasks: any;
30
paths: { [key: string]: Messages };
31
}
32
33
export class Lean extends EventEmitter {
34
paths: { [key: string]: SyncString } = {};
35
client: Client;
36
_server: LeanServer | undefined;
37
_state: State = { tasks: [], paths: {} };
38
private running: { [key: string]: number } = {};
39
dbg: Function;
40
41
constructor(client: Client) {
42
super();
43
this.server = reuseInFlight(this.server.bind(this));
44
this.client = client;
45
this.dbg = this.client.dbg("LEAN SERVER");
46
this.running = {};
47
}
48
49
close(): void {
50
this.kill();
51
close(this);
52
}
53
54
is_running(path: string): boolean {
55
return !!this.running[path] && now() - this.running[path] < SYNC_INTERVAL;
56
}
57
58
// nothing actually async in here... yet.
59
private async server(): Promise<LeanServer> {
60
if (this._server != undefined) {
61
if (this._server.alive()) {
62
return this._server;
63
}
64
// Kill cleans up any assumptions about stuff
65
// being sync'd.
66
this.kill();
67
// New server will now be created... below.
68
}
69
this._server = new lean_client.Server(
70
new lean_client.ProcessTransport(
71
"lean",
72
process.env.HOME ? process.env.HOME : ".", // satisfy typescript.
73
["-M 4096"],
74
),
75
);
76
this._server.error.on((err) => this.dbg("error:", err));
77
this._server.allMessages.on((allMessages) => {
78
this.dbg("messages: ", allMessages);
79
80
const new_messages = {};
81
for (const x of allMessages.msgs) {
82
const path: string = x.file_name;
83
delete x.file_name;
84
if (new_messages[path] === undefined) {
85
new_messages[path] = [x];
86
} else {
87
new_messages[path].push(x);
88
}
89
}
90
91
for (const path in this._state.paths) {
92
this.dbg("messages for ", path, new_messages[path]);
93
if (new_messages[path] === undefined) {
94
new_messages[path] = [];
95
}
96
this.dbg(
97
"messages for ",
98
path,
99
new_messages[path],
100
this._state.paths[path],
101
);
102
// length 0 is a special case needed when going from pos number of messages to none.
103
if (
104
new_messages[path].length === 0 ||
105
!isEqual(this._state.paths[path], new_messages[path])
106
) {
107
this.dbg("messages for ", path, "EMIT!");
108
this.emit("messages", path, new_messages[path]);
109
this._state.paths[path] = new_messages[path];
110
}
111
}
112
});
113
114
this._server.tasks.on((currentTasks) => {
115
const { tasks } = currentTasks;
116
this.dbg("tasks: ", tasks);
117
const running = {};
118
for (const task of tasks) {
119
running[task.file_name] = true;
120
}
121
for (const path in running) {
122
const v: any[] = [];
123
for (const task of tasks) {
124
if (task.file_name === path) {
125
delete task.file_name; // no longer needed
126
v.push(task);
127
}
128
}
129
this.emit("tasks", path, v);
130
}
131
for (const path in this.running) {
132
if (!running[path]) {
133
this.dbg("server", path, " done; no longer running");
134
this.running[path] = 0;
135
this.emit("tasks", path, []);
136
if (this.paths[path].changed) {
137
// file changed while lean was running -- so run lean again.
138
this.dbg(
139
"server",
140
path,
141
" changed while running, so running again",
142
);
143
this.paths[path].on_change();
144
}
145
}
146
}
147
});
148
this._server.connect();
149
return this._server;
150
}
151
152
// Start learn server parsing and reporting info about the given file.
153
// It will get updated whenever the file change.
154
async register(path: string): Promise<void> {
155
this.dbg("register", path);
156
if (this.paths[path] !== undefined) {
157
this.dbg("register", path, "already registered");
158
return;
159
}
160
// get the syncstring and start updating based on content
161
let syncstring: any = undefined;
162
while (syncstring == null) {
163
// todo change to be event driven!
164
syncstring = this.client.syncdoc({ path });
165
if (syncstring == null) {
166
await delay(1000);
167
} else if (syncstring.get_state() != "ready") {
168
await once(syncstring, "ready");
169
}
170
}
171
const on_change = async () => {
172
this.dbg("sync", path);
173
if (syncstring._closed) {
174
this.dbg("sync", path, "closed");
175
return;
176
}
177
if (this.is_running(path)) {
178
// already running, so do nothing - it will rerun again when done with current run.
179
this.dbg("sync", path, "already running");
180
this.paths[path].changed = true;
181
return;
182
}
183
184
const value: string = syncstring.to_str();
185
if (this.paths[path].last_value === value) {
186
this.dbg("sync", path, "skipping sync since value did not change");
187
return;
188
}
189
if (value.trim() === "") {
190
this.dbg(
191
"sync",
192
path,
193
"skipping sync document is empty (and LEAN behaves weird in this case)",
194
);
195
this.emit("sync", path, syncstring.hash_of_live_version());
196
return;
197
}
198
this.paths[path].last_value = value;
199
this._state.paths[path] = [];
200
this.running[path] = now();
201
this.paths[path].changed = false;
202
this.dbg("sync", path, "causing server sync now");
203
await (await this.server()).sync(path, value);
204
this.emit("sync", path, syncstring.hash_of_live_version());
205
};
206
this.paths[path] = {
207
syncstring,
208
on_change,
209
};
210
syncstring.on("change", on_change);
211
if (!syncstring._closed) {
212
on_change();
213
}
214
syncstring.on("closed", () => {
215
this.unregister(path);
216
});
217
}
218
219
// Stop updating given file on changes.
220
unregister(path: string): void {
221
this.dbg("unregister", path);
222
if (!this.paths[path]) {
223
// not watching it
224
return;
225
}
226
const x = this.paths[path];
227
delete this.paths[path];
228
delete this.running[path];
229
x.syncstring.removeListener("change", x.on_change);
230
x.syncstring.close();
231
}
232
233
// Kill the lean server and unregister all paths.
234
kill(): void {
235
this.dbg("kill");
236
if (this._server != undefined) {
237
for (const path in this.paths) {
238
this.unregister(path);
239
}
240
this._server.dispose();
241
delete this._server;
242
}
243
}
244
245
async restart(): Promise<void> {
246
this.dbg("restart");
247
if (this._server != undefined) {
248
for (const path in this.paths) {
249
this.unregister(path);
250
}
251
await this._server.restart();
252
}
253
}
254
255
async info(
256
path: string,
257
line: number,
258
column: number,
259
): Promise<lean_client.InfoResponse> {
260
this.dbg("info", path, line, column);
261
if (!this.paths[path]) {
262
this.register(path);
263
await callback((cb) => this.once(`sync-#{path}`, cb));
264
}
265
return await (await this.server()).info(path, line, column);
266
}
267
268
async complete(
269
path: string,
270
line: number,
271
column: number,
272
skipCompletions?: boolean,
273
): Promise<lean_client.CompleteResponse> {
274
this.dbg("complete", path, line, column);
275
if (!this.paths[path]) {
276
this.register(path);
277
await callback((cb) => this.once(`sync-#{path}`, cb));
278
}
279
const resp = await (
280
await this.server()
281
).complete(path, line, column, skipCompletions);
282
//this.dbg("complete response", path, line, column, resp);
283
return resp;
284
}
285
286
async version(): Promise<string> {
287
return (await this.server()).getVersion();
288
}
289
290
// Return state of parsing for everything that is currently registered.
291
state(): State {
292
return this._state;
293
}
294
295
messages(path: string): any[] {
296
const x = this._state.paths[path];
297
if (x !== undefined) {
298
return x;
299
}
300
return [];
301
}
302
}
303
304
let singleton: Lean | undefined;
305
306
// Return the singleton lean instance. The client is assumed to never change.
307
export function lean_server(client: Client): Lean {
308
if (singleton === undefined) {
309
singleton = new Lean(client);
310
}
311
return singleton;
312
}
313
314
function now(): number {
315
return Date.now();
316
}
317
318