Path: blob/master/src/packages/frontend/client/client.ts
1503 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/4import { bind_methods } from "@cocalc/util/misc";5import { EventEmitter } from "events";6import { delay } from "awaiting";7import { alert_message } from "../alerts";8import { ProjectCollaborators } from "./project-collaborators";9import { Messages } from "./messages";10import { QueryClient } from "./query";11import { TimeClient } from "./time";12import { AccountClient } from "./account";13import { ProjectClient } from "./project";14import { AdminClient } from "./admin";15import { LLMClient } from "./llm";16import { PurchasesClient } from "./purchases";17import { SyncClient } from "@cocalc/sync/client/sync-client";18import { UsersClient } from "./users";19import { FileClient } from "./file";20import { TrackingClient } from "./tracking";21import { ConatClient } from "@cocalc/frontend/conat/client";22import { IdleClient } from "./idle";23import { version } from "@cocalc/util/smc-version";24import { setup_global_cocalc } from "./console";25import { Query } from "@cocalc/sync/table";26import debug from "debug";27import Cookies from "js-cookie";28import { basePathCookieName } from "@cocalc/util/misc";29import { ACCOUNT_ID_COOKIE_NAME } from "@cocalc/util/db-schema/accounts";30import { appBasePath } from "@cocalc/frontend/customize/app-base-path";31import type { ConatSyncTableFunction } from "@cocalc/conat/sync/synctable";32import type {33CallConatServiceFunction,34CreateConatServiceFunction,35} from "@cocalc/conat/service";36import { randomId } from "@cocalc/conat/names";3738// This DEBUG variable comes from webpack:39declare const DEBUG;4041const log = debug("cocalc");42// To get actual extreme logging though you have to also set43//44// localStorage.DEBUG='cocalc'45//46// and refresh your browser. Example, this will turn on47// all the sync activity logging and everything that calls48// client.dbg.4950export const ACCOUNT_ID_COOKIE = decodeURIComponent(51basePathCookieName({52basePath: appBasePath,53name: ACCOUNT_ID_COOKIE_NAME,54}),55);5657export type AsyncCall = (opts: object) => Promise<any>;5859export interface WebappClient extends EventEmitter {60account_id?: string;61browser_id: string;62project_collaborators: ProjectCollaborators;63messages: Messages;64query_client: QueryClient;65time_client: TimeClient;66account_client: AccountClient;67project_client: ProjectClient;68admin_client: AdminClient;69openai_client: LLMClient;70purchases_client: PurchasesClient;71sync_client: SyncClient;72users_client: UsersClient;73file_client: FileClient;74tracking_client: TrackingClient;75conat_client: ConatClient;76idle_client: IdleClient;77client: Client;7879sync_string: Function;80sync_db: Function;8182server_time: Function;83get_username: Function;84is_signed_in: () => boolean;85synctable_project: Function;86synctable_conat: ConatSyncTableFunction;87callConatService: CallConatServiceFunction;88createConatService: CreateConatServiceFunction;89pubsub_conat: Function;90prettier: Function;91exec: Function;92touch_project: (project_id: string, compute_server_id?: number) => void;93ipywidgetsGetBuffer: (94project_id: string,95path: string,96model_id: string,97buffer_path: string,98) => Promise<ArrayBuffer>;99log_error: (any) => void;100user_tracking: Function;101send: Function;102call: Function;103dbg: (str: string) => Function;104is_project: () => boolean;105is_browser: () => boolean;106is_compute_server: () => boolean;107is_connected: () => boolean;108query: Query; // TODO typing109query_cancel: Function;110is_deleted: (filename: string, project_id: string) => boolean;111set_deleted: Function;112mark_file: (opts: any) => Promise<void>;113set_connected?: Function;114version: Function;115alert_message: Function;116}117118export const WebappClient = null; // webpack + TS es2020 modules need this119120/*121Connection events:122- 'connecting' -- trying to establish a connection123- 'connected' -- succesfully established a connection; data is the protocol as a string124- 'error' -- called when an error occurs125- 'output' -- received some output for stateless execution (not in any session)126- 'execute_javascript' -- code that server wants client to run (not for a particular session)127- 'message' -- emitted when a JSON message is received on('message', (obj) -> ...)128- 'data' -- emitted when raw data (not JSON) is received -- on('data, (id, data) -> )...129- 'signed_in' -- server pushes a succesful sign in to the client (e.g., due to130'remember me' functionality); data is the signed_in message.131- 'project_list_updated' -- sent whenever the list of projects owned by this user132changed; data is empty -- browser could ignore this unless133the project list is currently being displayed.134- 'project_data_changed - sent when data about a specific project has changed,135e.g., title/description/settings/etc.136- 'new_version', number -- sent when there is a new version of the source code so client should refresh137*/138139class Client extends EventEmitter implements WebappClient {140account_id: string = Cookies.get(ACCOUNT_ID_COOKIE);141browser_id: string = randomId();142project_collaborators: ProjectCollaborators;143messages: Messages;144query_client: QueryClient;145time_client: TimeClient;146account_client: AccountClient;147project_client: ProjectClient;148admin_client: AdminClient;149openai_client: LLMClient;150purchases_client: PurchasesClient;151sync_client: SyncClient;152users_client: UsersClient;153file_client: FileClient;154tracking_client: TrackingClient;155conat_client: ConatClient;156idle_client: IdleClient;157client: Client;158159sync_string: Function;160sync_db: Function;161162server_time: Function; // TODO: make this () => Date and deal with the fallout163get_username: Function;164is_signed_in: () => boolean;165synctable_project: Function;166synctable_conat: ConatSyncTableFunction;167callConatService: CallConatServiceFunction;168createConatService: CreateConatServiceFunction;169pubsub_conat: Function;170prettier: Function;171exec: Function;172touch_project: (project_id: string, compute_server_id?: number) => void;173ipywidgetsGetBuffer: (174project_id: string,175path: string,176model_id: string,177buffer_path: string,178) => Promise<ArrayBuffer>;179180log_error: (any) => void;181user_tracking: Function;182send: Function;183call: Function;184is_connected: () => boolean;185query: typeof QueryClient.prototype.query;186query_cancel: Function;187188is_deleted: (filename: string, project_id: string) => boolean;189mark_file: (opts: any) => Promise<void>;190191idle_reset: Function;192latency: Function;193synctable_database: Function;194async_query: Function;195alert_message: Function;196197constructor() {198super();199if (DEBUG) {200this.dbg = this.dbg.bind(this);201} else {202this.dbg = (..._) => {203return (..._) => {};204};205}206this.messages = new Messages();207this.query_client = bind_methods(new QueryClient(this));208this.time_client = bind_methods(new TimeClient(this));209this.account_client = bind_methods(new AccountClient(this));210this.project_client = bind_methods(new ProjectClient(this));211212this.sync_client = bind_methods(new SyncClient(this));213this.sync_string = this.sync_client.sync_string;214this.sync_db = this.sync_client.sync_db;215216this.admin_client = bind_methods(new AdminClient(this));217this.openai_client = bind_methods(new LLMClient(this));218this.purchases_client = bind_methods(new PurchasesClient(this));219this.users_client = bind_methods(new UsersClient(this));220this.tracking_client = bind_methods(new TrackingClient(this));221this.conat_client = bind_methods(new ConatClient(this));222this.is_signed_in = this.conat_client.is_signed_in.bind(this.conat_client);223this.is_connected = this.conat_client.is_connected.bind(this.conat_client);224this.file_client = bind_methods(new FileClient());225this.idle_client = bind_methods(new IdleClient(this));226this.project_collaborators = bind_methods(new ProjectCollaborators(this)); // must be after this.conat_client is defined.227228// Expose a public API as promised by WebappClient229this.server_time = this.time_client.server_time.bind(this.time_client);230231this.idle_reset = this.idle_client.idle_reset.bind(this.idle_client);232233this.exec = this.project_client.exec.bind(this.project_client);234this.touch_project = this.project_client.touch_project.bind(235this.project_client,236);237this.ipywidgetsGetBuffer = this.project_client.ipywidgetsGetBuffer.bind(238this.project_client,239);240241this.synctable_database = this.sync_client.synctable_database.bind(242this.sync_client,243);244this.synctable_conat = this.conat_client.synctable;245this.pubsub_conat = this.conat_client.pubsub;246this.callConatService = this.conat_client.callConatService;247this.createConatService = this.conat_client.createConatService;248249this.query = this.query_client.query.bind(this.query_client);250this.async_query = this.query_client.query.bind(this.query_client);251this.query_cancel = this.query_client.cancel.bind(this.query_client);252253this.is_deleted = this.file_client.is_deleted.bind(this.file_client);254this.mark_file = this.file_client.mark_file.bind(this.file_client);255256this.alert_message = alert_message;257258// Tweaks the maximum number of listeners an EventEmitter can have --259// 0 would mean unlimited260// The issue is https://github.com/sagemathinc/cocalc/issues/1098 and261// the errors we got are262// (node) warning: possible EventEmitter memory leak detected.263// 301 listeners added.264// Use emitter.setMaxListeners() to increase limit.265// every open file/table/sync db listens for connect event, which adds up.266this.setMaxListeners(3000);267268this.init_global_cocalc();269270bind_methods(this);271}272273private async init_global_cocalc(): Promise<void> {274await delay(1);275setup_global_cocalc(this);276}277278public dbg(f): Function {279if (log.enabled) {280return (...args) => log(new Date().toISOString(), f, ...args);281} else {282return (..._) => {};283}284// return function (...m) {285// console.log(`${new Date().toISOString()} - Client.${f}: `, ...m);286// };287}288289public version(): number {290return version;291}292293// account_id of this client294public client_id(): string | undefined {295return this.account_id;296}297298// false since this client is not a project299public is_project(): boolean {300return false;301}302303public is_browser(): boolean {304return true;305}306307public is_compute_server(): boolean {308return false;309}310311// true since this client is a user312public is_user(): boolean {313return true;314}315316public set_deleted(): void {317throw Error("not implemented for frontend");318}319320touchOpenFile = async ({321project_id,322path,323setNotDeleted,324doctype,325}: {326project_id: string;327path: string;328id?: number;329doctype?;330// if file is deleted, this explicitly undeletes it.331setNotDeleted?: boolean;332}) => {333const x = await this.conat_client.openFiles(project_id);334if (setNotDeleted) {335x.setNotDeleted(path);336}337x.touch(path, doctype);338};339}340341export const webapp_client = new Client();342343344