Path: blob/master/src/packages/frontend/conat/listings.ts
1503 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { TypedMap, redux } from "@cocalc/frontend/app-framework";6import { webapp_client } from "@cocalc/frontend/webapp-client";7import { Listing } from "@cocalc/util/db-schema/listings";8import type { DirectoryListingEntry } from "@cocalc/util/types";9import { EventEmitter } from "events";10import { fromJS, List } from "immutable";11import {12listingsClient,13type ListingsClient,14createListingsApiClient,15type ListingsApi,16MIN_INTEREST_INTERVAL_MS,17} from "@cocalc/conat/service/listings";18import { delay } from "awaiting";19import { reuseInFlight } from "@cocalc/util/reuse-in-flight";20import { getLogger } from "@cocalc/conat/client";2122const logger = getLogger("listings");2324export const WATCH_THROTTLE_MS = MIN_INTEREST_INTERVAL_MS;2526type ImmutablePathEntry = TypedMap<DirectoryListingEntry>;2728type State = "init" | "ready" | "closed";2930export type ImmutableListing = TypedMap<Listing>;3132export class Listings extends EventEmitter {33private project_id: string;34private compute_server_id: number;35private state: State = "init";36private listingsClient?: ListingsClient;37private api: ListingsApi;3839constructor(project_id: string, compute_server_id: number = 0) {40super();41this.project_id = project_id;42this.compute_server_id = compute_server_id;43this.api = createListingsApiClient({ project_id, compute_server_id });44this.init();45}4647private createClient = async () => {48let d = 3000;49const MAX_DELAY_MS = 15000;50while (this.state != "closed") {51try {52this.listingsClient = await listingsClient({53project_id: this.project_id,54compute_server_id: this.compute_server_id,55});56// success!57return;58} catch (err) {59logger.debug(60`WARNING: temporary issue connecting to project listings service -- ${err}`,61);62}63if (this.state == ("closed" as State)) return;64d = Math.min(MAX_DELAY_MS, d * 1.3);65await delay(d);66}67};6869private init = reuseInFlight(async () => {70//let start = Date.now();71await this.createClient();72// console.log("createClient finished in ", Date.now() - start, "ms");73if (this.state == "closed") return;74if (this.listingsClient == null) {75throw Error("bug");76}77this.listingsClient.on("change", (path) => {78this.emit("change", [path]);79});80// cause load of all cached data into redux81this.emit("change", Object.keys(this.listingsClient.getAll()));82// [ ] TODO: delete event for deleted paths83this.setState("ready");84});8586// Watch directory for changes.87watch = reuseInFlight(async (path: string, force?): Promise<void> => {88if (this.state == "closed") {89return;90}91if (this.state != "ready") {92await this.init();93}94if (this.state != "ready") {95// failed forever or closed explicitly so don't care96return;97}98if (this.listingsClient == null) {99throw Error("listings not ready");100}101if (this.listingsClient == null) return;102while (this.state != ("closed" as any) && this.listingsClient != null) {103try {104await this.listingsClient.watch(path, force);105return;106} catch (err) {107if (this.listingsClient == null) {108return;109}110force = true;111logger.debug(112`WARNING: not yet able to watch '${path}' in ${this.project_id} -- ${err}`,113);114try {115await this.listingsClient.api.conat.waitFor({116maxWait: 7.5 * 1000 * 60,117});118} catch (err) {119console.log(`WARNING -- waiting for directory listings -- ${err}`);120await delay(3000);121}122}123}124});125126get = async (127path: string,128trigger_start_project?: boolean,129): Promise<DirectoryListingEntry[] | undefined> => {130if (this.listingsClient == null) {131throw Error("listings not ready");132}133const x = this.listingsClient?.get(path);134if (x != null) {135if (x.error) {136throw Error(x.error);137}138if (!x.exists) {139throw Error(`ENOENT: no such directory '${path}'`);140}141return x.files;142}143if (trigger_start_project) {144if (145!(await redux.getActions("projects").start_project(this.project_id))146) {147return;148}149}150return await this.api.getListing({ path, hidden: true });151};152153// Does a call to the project to directly determine whether or154// not the given path exists. This doesn't depend on the table.155// Can throw an exception if it can't contact the project.156exists = async (path: string): Promise<boolean> => {157return (158(159await webapp_client.exec({160project_id: this.project_id,161command: "test",162args: ["-e", path],163err_on_exit: false,164})165).exit_code == 0166);167};168169// Returns:170// - List<ImmutablePathEntry> in case of a proper directory listing171// - string in case of an error172// - undefined if directory listing not known (and error not known either).173getForStore = async (174path: string,175): Promise<List<ImmutablePathEntry> | undefined | string> => {176try {177const x = await this.get(path);178return fromJS(x) as unknown as List<ImmutablePathEntry>;179} catch (err) {180return `${err}`;181}182};183184getUsingDatabase = async (185path: string,186): Promise<DirectoryListingEntry[] | undefined> => {187if (this.listingsClient == null) {188return;189}190return this.listingsClient.get(path)?.files;191};192193// TODO: we now only know there are more, not how many194getMissingUsingDatabase = async (195path: string,196): Promise<number | undefined> => {197if (this.listingsClient == null) {198// throw Error("listings not ready");199return;200}201return this.listingsClient.get(path)?.more ? 1 : 0;202};203204getMissing = (path: string): number | undefined => {205if (this.listingsClient == null) {206return;207}208return this.listingsClient.get(path)?.more ? 1 : 0;209};210211getListingDirectly = async (212path: string,213trigger_start_project?: boolean,214): Promise<DirectoryListingEntry[]> => {215// console.trace("getListingDirectly", { path });216if (trigger_start_project) {217if (218!(await redux.getActions("projects").start_project(this.project_id))219) {220throw Error("project not running");221}222}223return await this.api.getListing({ path, hidden: true });224};225226close = (): void => {227if (this.state == "closed") {228return;229}230this.setState("closed");231this.listingsClient?.close();232delete this.listingsClient;233};234235isReady = (): boolean => {236return this.state == ("ready" as State);237};238239setState = (state: State) => {240this.state = state;241this.emit(state);242};243}244245export function listings(246project_id: string,247compute_server_id: number = 0,248): Listings {249return new Listings(project_id, compute_server_id);250}251252253