Path: blob/master/src/packages/conat/sync/inventory.ts
1452 views
/*1Inventory of all streams and key:value stores in a specific project or account.23DEVELOPMENT:45i = await require('@cocalc/backend/conat/sync').inventory({project_id:'00847397-d6a8-4cb0-96a8-6ef64ac3e6cf'})67i.ls()89*/1011import { dkv, type DKV } from "./dkv";12import { dstream, type DStream } from "./dstream";13import { dko, type DKO } from "./dko";14import getTime from "@cocalc/conat/time";15import refCache from "@cocalc/util/refcache";16import type { JSONValue } from "@cocalc/util/types";17import {18human_readable_size as humanReadableSize,19trunc_middle,20} from "@cocalc/util/misc";21import { DKO_PREFIX } from "./dko";22import { waitUntilTimeAvailable } from "@cocalc/conat/time";23import {24type Configuration,25type PartialInventory,26} from "@cocalc/conat/persist/storage";27import { AsciiTable3 } from "ascii-table3";2829export const INVENTORY_UPDATE_INTERVAL = 90000;30export const INVENTORY_NAME = "CoCalc-Inventory";3132type Sort =33| "last"34| "created"35| "count"36| "bytes"37| "name"38| "type"39| "-last"40| "-created"41| "-count"42| "-bytes"43| "-name"44| "-type";4546interface Location {47account_id?: string;48project_id?: string;49}5051type StoreType = "stream" | "kv";5253export interface InventoryItem extends PartialInventory {54// when it was created55created: number;56// last time this stream was updated57last: number;58// optional description, which can be anything59desc?: JSONValue;60}6162interface FullItem extends InventoryItem {63type: StoreType;64name: string;65}6667export class Inventory {68public location: Location;69private dkv?: DKV<InventoryItem>;7071constructor(location: { account_id?: string; project_id?: string }) {72this.location = location;73}7475init = async () => {76this.dkv = await dkv({77name: INVENTORY_NAME,78...this.location,79});80await waitUntilTimeAvailable();81};8283// Set but with NO LIMITS and no MERGE conflict algorithm. Use with care!84set = ({85type,86name,87bytes,88count,89desc,90limits,91seq,92}: {93type: StoreType;94name: string;95bytes: number;96count: number;97limits: Partial<Configuration>;98desc?: JSONValue;99seq: number;100}) => {101if (this.dkv == null) {102throw Error("not initialized");103}104const last = getTime();105const key = this.encodeKey({ name, type });106const cur = this.dkv.get(key);107const created = cur?.created ?? last;108desc = desc ?? cur?.desc;109this.dkv.set(key, {110desc,111last,112created,113bytes,114count,115limits,116seq,117});118};119120private encodeKey = ({ name, type }) => JSON.stringify({ name, type });121122private decodeKey = (key) => JSON.parse(key);123124delete = ({ name, type }: { name: string; type: StoreType }) => {125if (this.dkv == null) {126throw Error("not initialized");127}128this.dkv.delete(this.encodeKey({ name, type }));129};130131get = (132x: { name: string; type: StoreType } | string,133): (InventoryItem & { type: StoreType; name: string }) | undefined => {134if (this.dkv == null) {135throw Error("not initialized");136}137let cur;138let name, type;139if (typeof x == "string") {140// just the name -- we infer/guess the type141name = x;142type = "kv";143cur = this.dkv.get(this.encodeKey({ name, type }));144if (cur == null) {145type = "stream";146cur = this.dkv.get(this.encodeKey({ name, type }));147}148} else {149name = x.name;150cur = this.dkv.get(this.encodeKey(x));151}152if (cur == null) {153return;154}155return { ...cur, type, name };156};157158getStores = async ({159filter,160sort = "-last",161}: { filter?: string; sort?: Sort } = {}): Promise<162(DKV | DStream | DKO)[]163> => {164const v: (DKV | DStream | DKO)[] = [];165const all = this.getAll({ filter });166for (const key of this.sortedKeys(all, sort)) {167const x = all[key];168const { desc, name, type } = x;169if (type == "kv") {170if (name.startsWith(DKO_PREFIX)) {171v.push(await dko<any>({ name, ...this.location, desc }));172} else {173v.push(await dkv({ name, ...this.location, desc }));174}175} else if (type == "stream") {176v.push(await dstream({ name, ...this.location, desc }));177} else {178throw Error(`unknown store type '${type}'`);179}180}181return v;182};183184getAll = ({ filter }: { filter?: string } = {}): FullItem[] => {185if (this.dkv == null) {186throw Error("not initialized");187}188const all = this.dkv.getAll();189if (filter) {190filter = filter.toLowerCase();191}192const v: FullItem[] = [];193for (const key of Object.keys(all)) {194const { name, type } = this.decodeKey(key);195if (filter) {196const { desc } = all[key];197const s = `${desc ? JSON.stringify(desc) : ""} ${name}`.toLowerCase();198if (!s.includes(filter)) {199continue;200}201}202v.push({ ...all[key], name, type });203}204return v;205};206207close = async () => {208await this.dkv?.close();209delete this.dkv;210};211212private sortedKeys = (all, sort0: Sort) => {213let reverse: boolean, sort: string;214if (sort0[0] == "-") {215reverse = true;216sort = sort0.slice(1);217} else {218reverse = false;219sort = sort0;220}221// return keys of all, sorted as specified222const x: { k: string; v: any }[] = [];223for (const k in all) {224x.push({ k, v: { ...all[k], ...this.decodeKey(k) } });225}226x.sort((a, b) => {227const a0 = a.v[sort];228const b0 = b.v[sort];229if (a0 < b0) {230return -1;231}232if (a0 > b0) {233return 1;234}235return 0;236});237const y = x.map(({ k }) => k);238if (reverse) {239y.reverse();240}241return y;242};243244ls = ({245log = console.log,246filter,247noTrunc,248path: path0,249sort = "last",250noHelp,251}: {252log?: Function;253filter?: string;254noTrunc?: boolean;255path?: string;256sort?: Sort;257noHelp?: boolean;258} = {}) => {259if (this.dkv == null) {260throw Error("not initialized");261}262const all = this.dkv.getAll();263if (!noHelp) {264log(265"ls(opts: {filter?: string; noTrunc?: boolean; path?: string; sort?: 'last'|'created'|'count'|'bytes'|'name'|'type'|'-last'|...})",266);267}268269const rows: any[] = [];270for (const key of this.sortedKeys(all, sort)) {271const { last, created, count, bytes, desc, limits } = all[key];272if (path0 && desc?.["path"] != path0) {273continue;274}275let { name, type } = this.decodeKey(key);276if (name.startsWith(DKO_PREFIX)) {277type = "kvobject";278name = name.slice(DKO_PREFIX.length);279}280if (!noTrunc) {281name = trunc_middle(name, 50);282}283if (284filter &&285!`${desc ? JSON.stringify(desc) : ""} ${name}`286.toLowerCase()287.includes(filter.toLowerCase())288) {289continue;290}291rows.push([292type,293name,294dateToString(new Date(created)),295humanReadableSize(bytes),296count,297dateToString(new Date(last)),298desc ? JSON.stringify(desc) : "",299limits != null && Object.keys(limits).length > 0300? JSON.stringify(limits)301: "--",302]);303}304305const table = new AsciiTable3(306`Inventory for ${JSON.stringify(this.location)}`,307)308.setHeading(309"Type",310"Name",311"Created",312"Size",313"Count",314"Last Update",315"Desc",316"Limits",317)318.addRowMatrix(rows);319table.setStyle("unicode-round");320if (!noTrunc) {321table.setWidth(7, 50).setWrapped(1);322table.setWidth(8, 30).setWrapped(1);323}324log(table.toString());325};326}327328function dateToString(d: Date) {329return d.toISOString().replace("T", " ").replace("Z", "").split(".")[0];330}331332export const cache = refCache<Location & { noCache?: boolean }, Inventory>({333name: "inventory",334createObject: async (loc) => {335const k = new Inventory(loc);336await k.init();337return k;338},339});340341export async function inventory(options: Location = {}): Promise<Inventory> {342return await cache(options);343}344345346