Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/project/sync/usage-info.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45// usage info for a specific file path, derived from the more general project info,6// which includes all processes and other stats78import { SyncTable, SyncTableState } from "@cocalc/sync/table";9import { once } from "@cocalc/util/async-utils";10import { close, merge } from "@cocalc/util/misc";11import { UsageInfoServer } from "../usage-info";12import type { ImmutableUsageInfo, UsageInfo } from "@cocalc/util/types/project-usage-info";13import { getLogger } from "@cocalc/backend/logger";1415const L = getLogger("sync:usage-info");1617class UsageInfoTable {18private readonly table?: SyncTable; // might be removed by close()19private readonly project_id: string;20private readonly servers: { [path: string]: UsageInfoServer } = {};21private readonly log: Function;2223constructor(table: SyncTable, project_id: string) {24this.project_id = project_id;25this.log = L.extend("table").debug;26this.table = table;27this.setup_watchers();28}2930public close(): void {31this.log("close");32for (const path in this.servers) {33this.stop_server(path);34}35close(this);36}3738// Start watching any paths that have recent interest (so this is not39// in response to a *change* after starting).40private async setup_watchers(): Promise<void> {41if (this.table == null) return; // closed42if (this.table.get_state() == ("init" as SyncTableState)) {43await once(this.table, "state");44}45if (this.table.get_state() != ("connected" as SyncTableState)) {46return; // game over47}48this.table.get()?.forEach((val) => {49const path = val.get("path");50if (path == null) return;51if (this.servers[path] == null) return; // already watching52});53this.log("setting up 'on.change'");54this.table.on("change", this.handle_change_event.bind(this));55}5657private async remove_stale_servers(): Promise<void> {58if (this.table == null) return; // closed59if (this.table.get_state() != ("connected" as SyncTableState)) return;60const paths: string[] = [];61this.table.get()?.forEach((val) => {62const path = val.get("path");63if (path == null) return;64paths.push(path);65});66for (const path of Object.keys(this.servers)) {67if (!paths.includes(path)) {68this.stop_server(path);69}70}71}7273private is_ready(): boolean {74return !!this.table?.is_ready();75}7677private get_table(): SyncTable {78if (!this.is_ready() || this.table == null) {79throw Error("table not ready");80}81return this.table;82}8384async set(obj: { path: string; usage?: UsageInfo }): Promise<void> {85this.get_table().set(86merge({ project_id: this.project_id }, obj),87"shallow"88);89await this.get_table().save();90}9192public get(path: string): ImmutableUsageInfo | undefined {93const x = this.get_table().get(JSON.stringify([this.project_id, path]));94if (x == null) return x;95return x as unknown as ImmutableUsageInfo;96// NOTE: That we have to use JSON.stringify above is an ugly shortcoming97// of the get method in @cocalc/sync/table/synctable.ts98// that could probably be relatively easily fixed.99}100101private handle_change_event(keys: string[]): void {102// this.log("handle_change_event", JSON.stringify(keys));103for (const key of keys) {104this.handle_change(JSON.parse(key)[1]);105}106this.remove_stale_servers();107}108109private handle_change(path: string): void {110this.log("handle_change", path);111const cur = this.get(path);112if (cur == null) return;113// Make sure we watch this path for updates, since there is genuine current interest.114this.ensure_watching(path);115this.set({ path });116}117118private ensure_watching(path: string): void {119if (this.servers[path] != null) {120// We are already watching this path, so nothing more to do.121return;122}123124try {125this.start_watching(path);126} catch (err) {127this.log("failed to start watching", err);128}129}130131private start_watching(path: string): void {132this.log(`start_watching ${path}`);133if (this.servers[path] != null) return;134const server = new UsageInfoServer(path);135136server.on("usage", (usage: UsageInfo) => {137// this.log(`watching/usage:`, usage);138try {139if (!this.is_ready()) return;140this.set({ path, usage });141} catch (err) {142this.log(`compute_listing("${path}") error: "${err}"`);143}144});145146server.start();147148this.servers[path] = server;149}150151private stop_server(path: string): void {152const s = this.servers[path];153if (s == null) return;154delete this.servers[path];155s.stop();156this.remove_path(path);157}158159private async remove_path(path: string): Promise<void> {160if (!this.is_ready()) return;161this.log("remove_path", path);162await this.get_table().delete({ project_id: this.project_id, path });163}164}165166let usage_info_table: UsageInfoTable | undefined = undefined;167export function register_usage_info_table(168table: SyncTable,169project_id: string170): void {171L.debug("register_usage_info_table");172if (usage_info_table != null) {173// There was one sitting around wasting space so clean it up174// before making a new one.175usage_info_table.close();176}177usage_info_table = new UsageInfoTable(table, project_id);178}179180export function get_usage_info_table(): UsageInfoTable | undefined {181return usage_info_table;182}183184185