Path: blob/master/src/packages/project/conat/listings.ts
1447 views
/* Directory Listings12- A service "listings" in each project and compute server that users call to express3interest in a directory. When there is recent interest in a4directory, we watch it for changes.56- A DKV store keys paths in the filesystem and values the first7few hundred (ordered by recent) files in that directory, all relative8to the home directory.91011DEVELOPMENT:12131. Stop listings service running in the project by running this in your browser:1415await cc.client.conat_client.projectApi(cc.current()).system.terminate({service:'listings'})1617{status: 'terminated', service: 'listings'}1819202. Setup project environment variables as usual (see README.md)21223. Start your own server2324.../src/packages/project/conat$ node252627await require('@cocalc/project/conat/listings').init()2829*/3031import getListing from "@cocalc/backend/get-listing";32import {33createListingsService,34getListingsKV,35getListingsTimesKV,36MAX_FILES_PER_DIRECTORY,37INTEREST_CUTOFF_MS,38type Listing,39type Times,40} from "@cocalc/conat/service/listings";41import { compute_server_id, project_id } from "@cocalc/project/data";42import { init as initClient } from "@cocalc/project/client";43import { delay } from "awaiting";44import { type DKV } from "./sync";45import { type ConatService } from "@cocalc/conat/service";46import { MultipathWatcher } from "@cocalc/backend/path-watcher";47import getLogger from "@cocalc/backend/logger";48import { path_split } from "@cocalc/util/misc";4950const logger = getLogger("project:conat:listings");5152let service: ConatService | null;53export async function init() {54logger.debug("init: initializing");55initClient();5657service = await createListingsService({58project_id,59compute_server_id,60impl,61});62const L = new Listings();63await L.init();64listings = L;65logger.debug("init: fully ready");66}6768export async function close() {69service?.close();70listings?.close();71}7273let listings: Listings | null;7475const impl = {76// cause the directory listing key:value store to watch path77watch: async (path: string) => {78while (listings == null) {79await delay(3000);80}81listings.watch(path);82},8384getListing: async ({ path, hidden }) => {85return await getListing(path, hidden);86},87};8889export function isDeleted(filename: string) {90return listings?.isDeleted(filename);91}9293class Listings {94private listings: DKV<Listing>;9596private times: DKV<Times>;9798private watcher: MultipathWatcher;99100private state: "init" | "ready" | "closed" = "init";101102constructor() {103this.watcher = new MultipathWatcher();104this.watcher.on("change", this.updateListing);105}106107init = async () => {108logger.debug("Listings.init: start");109this.listings = await getListingsKV({ project_id, compute_server_id });110this.times = await getListingsTimesKV({ project_id, compute_server_id });111// start watching paths with recent interest112const cutoff = Date.now() - INTEREST_CUTOFF_MS;113const times = this.times.getAll();114for (const path in times) {115if ((times[path].interest ?? 0) >= cutoff) {116await this.updateListing(path);117}118}119this.monitorInterestLoop();120this.state = "ready";121logger.debug("Listings.init: done");122};123124private monitorInterestLoop = async () => {125while (this.state != "closed") {126const cutoff = Date.now() - INTEREST_CUTOFF_MS;127const times = this.times.getAll();128for (const path in times) {129if ((times[path].interest ?? 0) <= cutoff) {130if (this.watcher.has(path)) {131logger.debug("monitorInterestLoop: stop watching", { path });132this.watcher.delete(path);133}134}135}136await delay(30 * 1000);137}138};139140close = () => {141this.state = "closed";142this.watcher.close();143this.listings?.close();144this.times?.close();145};146147updateListing = async (path: string) => {148logger.debug("updateListing", { path });149path = canonicalPath(path);150this.watcher.add(canonicalPath(path));151const start = Date.now();152try {153let files = await getListing(path, true, {154limit: MAX_FILES_PER_DIRECTORY + 1,155});156const more = files.length == MAX_FILES_PER_DIRECTORY + 1;157if (more) {158files = files.slice(0, MAX_FILES_PER_DIRECTORY);159}160this.listings.set(path, {161files,162exists: true,163time: Date.now(),164more,165});166logger.debug("updateListing: success", {167path,168ms: Date.now() - start,169count: files.length,170more,171});172} catch (err) {173let error = `${err}`;174if (error.startsWith("Error: ")) {175error = error.slice("Error: ".length);176}177this.listings.set(path, {178error,179time: Date.now(),180exists: error.includes("ENOENT") ? false : undefined,181});182logger.debug("updateListing: error", {183path,184ms: Date.now() - start,185error,186});187}188};189190watch = async (path: string) => {191logger.debug("watch", { path });192path = canonicalPath(path);193this.times.set(path, { ...this.times.get(path), interest: Date.now() });194this.updateListing(path);195};196197isDeleted = (filename: string): boolean | undefined => {198if (this.listings == null) {199return undefined;200}201const { head: path, tail } = path_split(filename);202const listing = this.listings.get(path);203if (listing == null) {204return undefined;205}206if (listing.deleted?.includes(tail)) {207return true;208}209return false;210};211}212213// this does a tiny amount to make paths more canonical.214function canonicalPath(path: string): string {215if (path == "." || path == "~") {216return "";217}218return path;219}220221222