Path: blob/master/src/packages/conat/compute/manager.ts
1452 views
/*12Used mainly from a browser client frontend to manage what compute server3is used to edit a given file.45Access this in the browser for the project you have open:67> m = await cc.client.conat_client.computeServerManager({project_id:cc.current().project_id})89*/1011import { dkv, type DKV } from "@cocalc/conat/sync/dkv";12import { EventEmitter } from "events";13import { once, until } from "@cocalc/util/async-utils";1415type State = "init" | "connected" | "closed";1617export interface Info {18// id = compute server where this path should be opened19id: number;20}2122export interface Options {23project_id: string;24noAutosave?: boolean;25noCache?: boolean;26}2728export function computeServerManager(options: Options) {29return new ComputeServerManager(options);30}3132export class ComputeServerManager extends EventEmitter {33private dkv?: DKV<Info>;34private options: Options;35public state: State = "init";3637constructor(options: Options) {38super();39this.options = options;40// It's reasonable to have many clients, e.g., one for each open file41this.setMaxListeners(100);42this.init();43}4445waitUntilReady = async () => {46if (this.state == "closed") {47throw Error("closed");48} else if (this.state == "connected") {49return;50}51await once(this, "connected");52};5354save = async () => {55await this.dkv?.save();56};5758private initialized = false;59init = async () => {60if (this.initialized) {61throw Error("init can only be called once");62}63this.initialized = true;64await until(65async () => {66if (this.state != "init") {67return true;68}69const d = await dkv<Info>({70name: "compute-server-manager",71...this.options,72});73if (this.state == ("closed" as any)) {74d.close();75return true;76}77this.dkv = d;78d.on("change", this.handleChange);79this.setState("connected");80return true;81},82{83start: 3000,84decay: 1.3,85max: 15000,86log: (...args) =>87console.log(88"WARNING: issue creating compute server manager",89...args,90),91},92);93};9495private handleChange = ({ key: path, value, prev }) => {96this.emit("change", {97path,98id: value?.id,99prev_id: prev?.id,100});101};102103close = () => {104// console.log("close compute server manager", this.options);105if (this.dkv != null) {106this.dkv.removeListener("change", this.handleChange);107this.dkv.close();108delete this.dkv;109}110this.setState("closed");111this.removeAllListeners();112};113114private setState = (state: State) => {115this.state = state;116this.emit(state);117};118119private getDkv = () => {120if (this.dkv == null) {121const message = `compute server manager not initialized -- in state '${this.state}'`;122console.warn(message);123throw Error(message);124}125return this.dkv;126};127128set = (path, id) => {129const kv = this.getDkv();130if (!id) {131kv.delete(path);132return;133}134kv.set(path, { id });135};136137delete = (path) => {138this.getDkv().delete(path);139};140141get = (path) => this.getDkv().get(path)?.id;142143getAll = () => {144return this.getDkv().getAll();145};146147// Async API that doesn't assume manager has been initialized, with148// very long names. Used in the frontend.149150// Call this if you want the compute server with given id to151// connect and handle being the server for the given path.152connectComputeServerToPath = async ({153path,154id,155}: {156path: string;157id: number;158}) => {159try {160await this.waitUntilReady();161} catch {162return;163}164this.set(path, id);165};166167// Call this if you want no compute servers to provide the backend server168// for given path.169disconnectComputeServer = async ({ path }: { path: string }) => {170try {171await this.waitUntilReady();172} catch {173return;174}175this.delete(path);176};177178// Returns the explicitly set server id for the given179// path, if one is set. Otherwise, return undefined180// if nothing is explicitly set for this path (i.e., usually means home base).181getServerIdForPath = async (path: string): Promise<number | undefined> => {182try {183await this.waitUntilReady();184} catch {185return;186}187return this.get(path);188};189190// Get the server ids (as a map) for every file and every directory contained in path.191// NOTE/TODO: this just does a linear search through all paths with a server id; nothing clever.192getServerIdForSubtree = async (193path: string,194): Promise<{ [path: string]: number }> => {195await this.waitUntilReady();196if (this.state == "closed") {197throw Error("closed");198}199const kv = this.getDkv();200const v: { [path: string]: number } = {};201const slash = path.endsWith("/") ? path : path + "/";202const x = kv.getAll();203for (const p in x) {204if (p == path || p.startsWith(slash)) {205v[p] = x[p].id;206}207}208return v;209};210}211212213