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/sync/usage-info.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
// usage info for a specific file path, derived from the more general project info,
7
// which includes all processes and other stats
8
9
import { SyncTable, SyncTableState } from "@cocalc/sync/table";
10
import { once } from "@cocalc/util/async-utils";
11
import { close, merge } from "@cocalc/util/misc";
12
import { UsageInfoServer } from "../usage-info";
13
import type { ImmutableUsageInfo, UsageInfo } from "@cocalc/util/types/project-usage-info";
14
import { getLogger } from "@cocalc/backend/logger";
15
16
const L = getLogger("sync:usage-info");
17
18
class UsageInfoTable {
19
private readonly table?: SyncTable; // might be removed by close()
20
private readonly project_id: string;
21
private readonly servers: { [path: string]: UsageInfoServer } = {};
22
private readonly log: Function;
23
24
constructor(table: SyncTable, project_id: string) {
25
this.project_id = project_id;
26
this.log = L.extend("table").debug;
27
this.table = table;
28
this.setup_watchers();
29
}
30
31
public close(): void {
32
this.log("close");
33
for (const path in this.servers) {
34
this.stop_server(path);
35
}
36
close(this);
37
}
38
39
// Start watching any paths that have recent interest (so this is not
40
// in response to a *change* after starting).
41
private async setup_watchers(): Promise<void> {
42
if (this.table == null) return; // closed
43
if (this.table.get_state() == ("init" as SyncTableState)) {
44
await once(this.table, "state");
45
}
46
if (this.table.get_state() != ("connected" as SyncTableState)) {
47
return; // game over
48
}
49
this.table.get()?.forEach((val) => {
50
const path = val.get("path");
51
if (path == null) return;
52
if (this.servers[path] == null) return; // already watching
53
});
54
this.log("setting up 'on.change'");
55
this.table.on("change", this.handle_change_event.bind(this));
56
}
57
58
private async remove_stale_servers(): Promise<void> {
59
if (this.table == null) return; // closed
60
if (this.table.get_state() != ("connected" as SyncTableState)) return;
61
const paths: string[] = [];
62
this.table.get()?.forEach((val) => {
63
const path = val.get("path");
64
if (path == null) return;
65
paths.push(path);
66
});
67
for (const path of Object.keys(this.servers)) {
68
if (!paths.includes(path)) {
69
this.stop_server(path);
70
}
71
}
72
}
73
74
private is_ready(): boolean {
75
return !!this.table?.is_ready();
76
}
77
78
private get_table(): SyncTable {
79
if (!this.is_ready() || this.table == null) {
80
throw Error("table not ready");
81
}
82
return this.table;
83
}
84
85
async set(obj: { path: string; usage?: UsageInfo }): Promise<void> {
86
this.get_table().set(
87
merge({ project_id: this.project_id }, obj),
88
"shallow"
89
);
90
await this.get_table().save();
91
}
92
93
public get(path: string): ImmutableUsageInfo | undefined {
94
const x = this.get_table().get(JSON.stringify([this.project_id, path]));
95
if (x == null) return x;
96
return x as unknown as ImmutableUsageInfo;
97
// NOTE: That we have to use JSON.stringify above is an ugly shortcoming
98
// of the get method in @cocalc/sync/table/synctable.ts
99
// that could probably be relatively easily fixed.
100
}
101
102
private handle_change_event(keys: string[]): void {
103
// this.log("handle_change_event", JSON.stringify(keys));
104
for (const key of keys) {
105
this.handle_change(JSON.parse(key)[1]);
106
}
107
this.remove_stale_servers();
108
}
109
110
private handle_change(path: string): void {
111
this.log("handle_change", path);
112
const cur = this.get(path);
113
if (cur == null) return;
114
// Make sure we watch this path for updates, since there is genuine current interest.
115
this.ensure_watching(path);
116
this.set({ path });
117
}
118
119
private ensure_watching(path: string): void {
120
if (this.servers[path] != null) {
121
// We are already watching this path, so nothing more to do.
122
return;
123
}
124
125
try {
126
this.start_watching(path);
127
} catch (err) {
128
this.log("failed to start watching", err);
129
}
130
}
131
132
private start_watching(path: string): void {
133
this.log(`start_watching ${path}`);
134
if (this.servers[path] != null) return;
135
const server = new UsageInfoServer(path);
136
137
server.on("usage", (usage: UsageInfo) => {
138
// this.log(`watching/usage:`, usage);
139
try {
140
if (!this.is_ready()) return;
141
this.set({ path, usage });
142
} catch (err) {
143
this.log(`compute_listing("${path}") error: "${err}"`);
144
}
145
});
146
147
server.start();
148
149
this.servers[path] = server;
150
}
151
152
private stop_server(path: string): void {
153
const s = this.servers[path];
154
if (s == null) return;
155
delete this.servers[path];
156
s.stop();
157
this.remove_path(path);
158
}
159
160
private async remove_path(path: string): Promise<void> {
161
if (!this.is_ready()) return;
162
this.log("remove_path", path);
163
await this.get_table().delete({ project_id: this.project_id, path });
164
}
165
}
166
167
let usage_info_table: UsageInfoTable | undefined = undefined;
168
export function register_usage_info_table(
169
table: SyncTable,
170
project_id: string
171
): void {
172
L.debug("register_usage_info_table");
173
if (usage_info_table != null) {
174
// There was one sitting around wasting space so clean it up
175
// before making a new one.
176
usage_info_table.close();
177
}
178
usage_info_table = new UsageInfoTable(table, project_id);
179
}
180
181
export function get_usage_info_table(): UsageInfoTable | undefined {
182
return usage_info_table;
183
}
184
185