Path: blob/master/src/packages/frontend/client/query.ts
5691 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { is_array } from "@cocalc/util/misc";6import { validate_client_query } from "@cocalc/util/schema-validate";7import { CB } from "@cocalc/util/types/database";8import { ConatChangefeed } from "@cocalc/sync/table/changefeed-conat";9import { uuid } from "@cocalc/util/misc";1011declare const $: any; // jQuery1213export class QueryClient {14private client: any;15private changefeeds: { [id: string]: ConatChangefeed } = {};1617constructor(client: any) {18this.client = client;19}2021private doChangefeed = async (opts: {22query: object;23options?: object[];24cb: CB;25}): Promise<void> => {26let changefeed;27try {28changefeed = new ConatChangefeed({29account_id: this.client.account_id,30query: opts.query,31options: opts.options,32});33// id for canceling this changefeed34const id = uuid();35const initval = await changefeed.connect();36const query = {37[Object.keys(opts.query)[0]]: initval,38};39this.changefeeds[id] = changefeed;40opts.cb(undefined, { query, id });41changefeed.on("update", (change) => {42opts.cb(undefined, change);43});44} catch (err) {45opts.cb(`${err}`);46}47};4849private doQuery = async (opts: {50query: object;51options?: object[];52timeout?: number;53}): Promise<any> => {54let timer: ReturnType<typeof setTimeout> | undefined;5556try {57const queryPromise = this.client.conat_client.hub.db.userQuery({58query: opts.query,59options: opts.options,60timeout: opts.timeout,61});6263// Add client-side timeout if explicitly requested64if (opts.timeout != null) {65let timedOut = false;6667const timeoutPromise = new Promise<never>((_, reject) => {68timer = setTimeout(() => {69timedOut = true; // Set flag before rejecting70reject(new Error(`Query timed out after ${opts.timeout} ms`));71}, opts.timeout);72});7374// Prevent unhandled rejection if timeout fires first75queryPromise.catch((err) => {76if (timedOut) {77// Timeout already happened, this is an orphaned rejection - just log it78console.warn("Query failed after client-side timeout:", err);79}80// If not timed out, error is handled by the race81});8283return await Promise.race([queryPromise, timeoutPromise]);84} else {85return await queryPromise;86}87} finally {88if (timer != null) {89clearTimeout(timer);90}91}92};9394// This works like a normal async function when95// opts.cb is NOT specified. When opts.cb is specified,96// it works like a cb and returns nothing. For changefeeds97// you MUST specify opts.cb, but can always optionally do so.98query = async (opts: {99query: object;100options?: object[]; // if given must be an array of objects, e.g., [{limit:5}]101changes?: boolean;102timeout?: number; // ms103cb?: CB; // support old cb interface104}): Promise<any> => {105// Deprecation warnings:106for (const field of ["standby", "no_post", "ignore_response"]) {107if (opts[field] != null) {108console.trace(`WARNING: passing '${field}' to query is deprecated`);109}110}111if (opts.options != null && !is_array(opts.options)) {112// should never happen...113throw Error("options must be an array");114}115if (opts.changes) {116const { cb } = opts;117if (cb == null) {118throw Error("for changefeed, must specify opts.cb");119}120await this.doChangefeed({121query: opts.query,122options: opts.options,123cb,124});125} else {126try {127const err = validate_client_query(opts.query, this.client.account_id);128if (err) {129throw Error(err);130}131132const query = await this.doQuery({133query: opts.query,134options: opts.options,135timeout: opts.timeout,136});137138if (opts.cb == null) {139return { query };140} else {141opts.cb(undefined, { query });142}143} catch (err) {144if (opts.cb == null) {145throw err;146} else {147opts.cb(err);148}149}150}151};152153// cancel a changefeed created above. This is ONLY used154// right now by the CRM code.155cancel = async (id: string): Promise<void> => {156this.changefeeds[id]?.close();157delete this.changefeeds[id];158};159}160161162