Path: blob/master/src/packages/conat/service/listings.ts
1453 views
/*1Service for watching directory listings in a project or compute server.2*/34import { createServiceClient, createServiceHandler } from "./typed";5import type { DirectoryListingEntry } from "@cocalc/util/types";6import { dkv, type DKV } from "@cocalc/conat/sync/dkv";7import { EventEmitter } from "events";8import refCache from "@cocalc/util/refcache";910// record info about at most this many files in a given directory11//export const MAX_FILES_PER_DIRECTORY = 10;12export const MAX_FILES_PER_DIRECTORY = 500;1314// cache listing info about at most this many directories15//export const MAX_DIRECTORIES = 3;16export const MAX_DIRECTORIES = 50;1718// watch directories with interest that are this recent19//export const INTEREST_CUTOFF_MS = 1000 * 30;20export const INTEREST_CUTOFF_MS = 1000 * 60 * 10;2122export const MIN_INTEREST_INTERVAL_MS = 15 * 1000;2324export interface ListingsApi {25// cause the directory listing key:value store to watch path26watch: (path: string) => Promise<void>;2728// just directly get the listing info now for this path29getListing: (opts: {30path: string;31hidden?: boolean;32}) => Promise<DirectoryListingEntry[]>;33}3435interface ListingsOptions {36project_id: string;37compute_server_id?: number;38}3940export function createListingsApiClient({41project_id,42compute_server_id = 0,43}: ListingsOptions) {44return createServiceClient<ListingsApi>({45project_id,46compute_server_id,47service: "listings",48});49}5051export type ListingsServiceApi = ReturnType<typeof createListingsApiClient>;5253export async function createListingsService({54project_id,55compute_server_id = 0,56impl,57}: ListingsOptions & { impl }) {58const c = compute_server_id ? ` (compute server: ${compute_server_id})` : "";59return await createServiceHandler<ListingsApi>({60project_id,61compute_server_id,62service: "listings",63description: `Directory listing service: ${c}`,64impl,65});66}6768const config = {69max_msgs: MAX_DIRECTORIES,70};7172export interface Listing {73files?: DirectoryListingEntry[];74exists?: boolean;75error?: string;76time: number;77more?: boolean;78deleted?: string[];79}8081export async function getListingsKV(82opts: ListingsOptions,83): Promise<DKV<Listing>> {84return await dkv<Listing>({85name: "listings",86config,87...opts,88});89}9091export interface Times {92// time last files for a given directory were attempted to be updated93updated?: number;94// time user requested to watch a given directory95interest?: number;96}9798export async function getListingsTimesKV(99opts: ListingsOptions,100): Promise<DKV<Times>> {101return await dkv<Times>({102name: "listings-times",103config,104...opts,105});106}107108/* Unified interface to the above components for clients */109110export class ListingsClient extends EventEmitter {111options: { project_id: string; compute_server_id: number };112api: Awaited<ReturnType<typeof createListingsApiClient>>;113times?: DKV<Times>;114listings?: DKV<Listing>;115116constructor({117project_id,118compute_server_id = 0,119}: {120project_id: string;121compute_server_id?: number;122}) {123super();124this.options = { project_id, compute_server_id };125}126127init = async () => {128try {129this.api = createListingsApiClient(this.options);130this.times = await getListingsTimesKV(this.options);131this.listings = await getListingsKV(this.options);132this.listings.on("change", this.handleListingsChange);133} catch (err) {134this.close();135throw err;136}137};138139handleListingsChange = ({ key: path }) => {140this.emit("change", path);141};142143get = (path: string): Listing | undefined => {144if (this.listings == null) {145throw Error("not ready");146}147return this.listings.get(path);148};149150getAll = () => {151if (this.listings == null) {152throw Error("not ready");153}154return this.listings.getAll();155};156157close = () => {158this.removeAllListeners();159this.times?.close();160delete this.times;161if (this.listings != null) {162this.listings.removeListener("change", this.handleListingsChange);163this.listings.close();164delete this.listings;165}166};167168watch = async (path, force = false) => {169if (this.times == null) {170throw Error("not ready");171}172if (!force) {173const last = this.times.get(path)?.interest ?? 0;174if (Math.abs(Date.now() - last) < MIN_INTEREST_INTERVAL_MS) {175// somebody already expressed interest very recently176return;177}178}179await this.api.watch(path);180};181182getListing = async (opts) => {183return await this.api.getListing(opts);184};185}186187export const listingsClient = refCache<188ListingsOptions & { noCache?: boolean },189ListingsClient190>({191name: "listings",192createObject: async (options: ListingsOptions) => {193const C = new ListingsClient(options);194await C.init();195return C;196},197});198199200