@cocalc/project
Version:
CoCalc: project daemon
860 lines (782 loc) • 29.5 kB
JavaScript
// Generated by CoffeeScript 2.5.1
(function() {
//########################################################################
// This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
// License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
//########################################################################
/*
client.coffee -- A project viewed as a client for a hub.
For security reasons, a project does initiate a TCP connection to a hub,
but rather hubs initiate TCP connections to projects:
* MINUS: This makes various things more complicated, e.g., a project
might not have any open connection to a hub, but still "want" to write
something to the database; in such a case it is simply out of luck
and must wait.
* PLUS: Security is simpler since a hub initiates the connection to
a project. A hub doesn't have to receive TCP connections and decide
whether or not to trust what is on the other end of those connections.
That said, this architecture could change, and very little code would change
as a result.
*/
var ALREADY_CREATED, DEBUG, DEBUG_FILE, EventEmitter, PROJECT_HUB_HEARTBEAT_INTERVAL_S, Watcher, async, blobs, callback2, defaults, ensureContainingDirectoryExists, fs, getLogger, get_kernel_data, get_listings_table, get_syncdoc, join, json, jupyter, kucalc, message, misc, misc_node, once, ref, required, sage_session, schema, syncdb2, synctable2, winston, writeFile,
boundMethodCheck = function(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new Error('Bound instance method accessed before binding'); } };
({PROJECT_HUB_HEARTBEAT_INTERVAL_S} = require('@cocalc/util/heartbeat'));
fs = require('fs');
({join} = require('path'));
({EventEmitter} = require('events'));
({callback2, once} = require("@cocalc/util/async-utils"));
async = require('async');
message = require('@cocalc/util/message');
misc = require('@cocalc/util/misc');
misc_node = require('@cocalc/backend/misc_node');
synctable2 = require('@cocalc/sync/table');
syncdb2 = require('@cocalc/sync/editor/db');
schema = require('@cocalc/util/schema');
ensureContainingDirectoryExists = require('@cocalc/backend/misc/ensure-containing-directory-exists').default;
writeFile = require("fs/promises").writeFile;
sage_session = require('./sage_session');
jupyter = require('./jupyter/jupyter');
({get_kernel_data} = require('./jupyter/kernel-data'));
({json} = require('./common'));
kucalc = require('./kucalc');
({Watcher} = require('./watcher'));
blobs = require('./blobs');
({get_syncdoc} = require('./sync/sync-doc'));
({get_listings_table} = require('./sync/listings'));
({defaults, required} = misc);
({getLogger} = require('./logger'));
winston = getLogger('Client');
DEBUG = false;
// Easy way to enable debugging in any project anywhere.
DEBUG_FILE = join(process.env.HOME, '.smc-DEBUG');
if (fs.existsSync(DEBUG_FILE)) {
DEBUG = true;
} else if (kucalc.IN_KUCALC) {
// always make verbose in kucalc, since logs are taken care of by the k8s
// logging infrastructure...
DEBUG = true;
}
exports.init = () => {
return exports.client = new exports.Client();
};
ALREADY_CREATED = false;
ref = exports.Client = class Client extends EventEmitter {
constructor() {
var project_id;
super();
// use to define a logging function that is cleanly used internally
this.dbg = this.dbg.bind(this);
this.alert_message = this.alert_message.bind(this);
// todo: more could be closed...
this.close = this.close.bind(this);
// account_id or project_id of this client
this.client_id = this.client_id.bind(this);
// true since this client is a project
this.is_project = this.is_project.bind(this);
// false since this client is not a user
this.is_user = this.is_user.bind(this);
this.is_signed_in = this.is_signed_in.bind(this);
this.is_connected = this.is_connected.bind(this);
// We trust the time on our own compute servers (unlike random user's browser).
this.server_time = this.server_time.bind(this);
// Declare that the given socket is active right now and can be used for
// communication with some hub (the one the socket is connected to).
this.active_socket = this.active_socket.bind(this);
// Handle a mesg coming back from some hub. If we have a callback we call it
// for the given message, then return true. Otherwise, return
// false, meaning something else should try to handle this message.
this.handle_mesg = this.handle_mesg.bind(this);
// Get a socket connection to the hub from one in our cache; choose one at random.
// There is obviously no guarantee to get the same hub if you call this twice!
// Returns undefined if there are currently no connections from any hub to us
// (in which case, the project must wait).
this.get_hub_socket = this.get_hub_socket.bind(this);
// Send a message to some hub server and await a response (if cb defined).
this.call = this.call.bind(this);
// Do a project_query
this.query = this.query.bind(this);
// Cancel an outstanding changefeed query.
this._query_cancel = this._query_cancel.bind(this);
// ASYNC version
this.query_cancel = this.query_cancel.bind(this);
this.sync_table = this.sync_table.bind(this);
// We leave in the project_id for consistency with the browser UI.
// And maybe someday we'll have tables managed across projects (?).
this.synctable_project = this.synctable_project.bind(this);
// WARNING: making two of the exact same sync_string or sync_db will definitely
// lead to corruption!
// Get the synchronized doc with the given path. Returns undefined
// if currently no such sync-doc.
this.syncdoc = this.syncdoc.bind(this);
this.symmetric_channel = this.symmetric_channel.bind(this);
// Write a file to a given path (relative to env.HOME) on disk; will create containing directory.
// If file is currently being written or read in this process, will result in error (instead of silently corrupt data).
this.write_file = this.write_file.bind(this);
// Read file as a string from disk.
// If file is currently being written or read in this process,
// will retry until it isn't, so we do not get an error and we
// do NOT get silently corrupted data.
this.path_read = this.path_read.bind(this);
this.path_access = this.path_access.bind(this);
// TODO: exists is deprecated. "To check if a file exists
// without manipulating it afterwards, fs.access() is
// recommended."
this.path_exists = this.path_exists.bind(this);
this.path_stat = this.path_stat.bind(this);
// Size of file in bytes (divide by 1000 for K, by 10^6 for MB.)
this.file_size = this.file_size.bind(this);
// execute a command using the shell or a subprocess -- see docs for execute_code in misc_node.
this.shell = this.shell.bind(this);
// return new sage session
this.sage_session = this.sage_session.bind(this);
// returns a Jupyter kernel session
this.jupyter_kernel = this.jupyter_kernel.bind(this);
this.jupyter_kernel_info = this.jupyter_kernel_info.bind(this);
// See the file watcher.coffee for docs
this.watch_file = this.watch_file.bind(this);
// Save a blob to the central db blobstore.
// The sha1 is optional.
this.save_blob = this.save_blob.bind(this);
this.get_blob = this.get_blob.bind(this);
// no-op; assumed async api
this.touch_project = this.touch_project.bind(this);
// async
this.get_syncdoc_history = this.get_syncdoc_history.bind(this);
// NOTE: returns false if the listings table isn't connected.
this.is_deleted = this.is_deleted.bind(this);
this.set_deleted = this.set_deleted.bind(this);
if (ALREADY_CREATED) {
throw Error("BUG: Client already created!");
}
ALREADY_CREATED = true;
project_id = require('./data').project_id;
this.project_id = project_id;
this.dbg('constructor')();
this.setMaxListeners(300); // every open file/table/sync db listens for connect event, which adds up.
// initialize two caches
this._hub_callbacks = {};
this._hub_client_sockets = {};
this._changefeed_sockets = {};
this._connected = false;
this._winston = winston;
// Start listening for syncstrings that have been recently modified, so that we
// can open them and provide filesystem and computational support.
// TODO: delete this code.
//# @_init_recent_syncstrings_table()
if (kucalc.IN_KUCALC) {
kucalc.init(this);
}
}
dbg(f, trunc = 1000) {
boundMethodCheck(this, ref);
if (DEBUG && this._winston) {
return (...m) => {
var s;
switch (m.length) {
case 0:
s = '';
break;
case 1:
s = m[0];
break;
default:
s = JSON.stringify(m);
}
return this._winston.debug(`Client.${f}: ${misc.trunc_middle(s, trunc)}`);
};
} else {
return function(m) {};
}
}
alert_message(opts) {
boundMethodCheck(this, ref);
opts = defaults(opts, {
type: 'default',
title: void 0,
message: required,
block: void 0,
timeout: void 0 // time in seconds
});
return this.dbg('alert_message')(opts.title, opts.message);
}
close() {
var _, ref1, s;
boundMethodCheck(this, ref);
ref1 = misc.keys(this._open_syncstrings);
for (_ in ref1) {
s = ref1[_];
s.close();
}
delete this._open_syncstrings;
return clearInterval(this._recent_syncstrings_interval);
}
client_id() {
boundMethodCheck(this, ref);
return this.project_id;
}
is_project() {
boundMethodCheck(this, ref);
return true;
}
is_user() {
boundMethodCheck(this, ref);
return false;
}
is_signed_in() {
boundMethodCheck(this, ref);
return true;
}
is_connected() {
boundMethodCheck(this, ref);
return this._connected;
}
server_time() {
boundMethodCheck(this, ref);
return new Date();
}
active_socket(socket) {
var check_heartbeat, dbg, locals, socket_end, x;
boundMethodCheck(this, ref);
dbg = this.dbg(`active_socket(id=${socket.id},ip='${socket.remoteAddress}')`);
x = this._hub_client_sockets[socket.id];
if (x == null) {
dbg();
x = this._hub_client_sockets[socket.id] = {
socket: socket,
callbacks: {},
activity: new Date()
};
locals = {
heartbeat_interval: void 0
};
socket_end = () => {
var cb, id, ref1;
if (locals.heartbeat_interval == null) {
return;
}
dbg("ending socket");
clearInterval(locals.heartbeat_interval);
locals.heartbeat_interval = void 0;
if (x.callbacks != null) {
ref1 = x.callbacks;
for (id in ref1) {
cb = ref1[id];
if (typeof cb === "function") {
cb('socket closed');
}
}
delete x.callbacks; // so additional trigger of end doesn't do anything
}
delete this._hub_client_sockets[socket.id];
dbg(`number of active sockets now equals ${misc.len(this._hub_client_sockets)}`);
if (misc.len(this._hub_client_sockets) === 0) {
this._connected = false;
dbg("lost all active sockets");
this.emit('disconnected');
}
return socket.end();
};
socket.on('end', socket_end);
socket.on('error', socket_end);
check_heartbeat = () => {
if ((socket.heartbeat == null) || new Date() - socket.heartbeat >= 1.5 * PROJECT_HUB_HEARTBEAT_INTERVAL_S * 1000) {
dbg("heartbeat failed");
return socket_end();
} else {
return dbg("heartbeat -- socket is working");
}
};
locals.heartbeat_interval = setInterval(check_heartbeat, 1.5 * PROJECT_HUB_HEARTBEAT_INTERVAL_S * 1000);
if (misc.len(this._hub_client_sockets) >= 1) {
dbg("CONNECTED!");
this._connected = true;
return this.emit('connected');
}
} else {
return x.activity = new Date();
}
}
handle_mesg(mesg, socket) {
var dbg, err, f;
boundMethodCheck(this, ref);
dbg = this.dbg(`handle_mesg(${misc.trunc_middle(json(mesg), 512)})`);
f = this._hub_callbacks[mesg.id];
if (f != null) {
dbg("calling callback");
if (!mesg.multi_response) {
delete this._hub_callbacks[mesg.id];
delete this._hub_client_sockets[socket.id].callbacks[mesg.id];
}
try {
f(mesg);
} catch (error) {
err = error;
dbg(`WARNING: error handling message from client. -- ${err}`);
}
return true;
} else {
dbg("no callback");
return false;
}
}
get_hub_socket() {
var socket_ids;
boundMethodCheck(this, ref);
socket_ids = misc.keys(this._hub_client_sockets);
this.dbg("get_hub_socket")(`there are ${socket_ids.length} sockets -- ${JSON.stringify(socket_ids)}`);
if (socket_ids.length === 0) {
return;
}
return this._hub_client_sockets[misc.random_choice(socket_ids)].socket;
}
call(opts) {
var base, cb, dbg, fail, socket, timer;
boundMethodCheck(this, ref);
opts = defaults(opts, {
message: required,
timeout: void 0, // timeout in seconds; if specified call will error out after this much time
socket: void 0, // if specified, use this socket
cb: void 0 // awaits response if given
});
dbg = this.dbg(`call(message=${json(opts.message)})`);
dbg();
socket = opts.socket != null ? opts.socket : opts.socket = this.get_hub_socket(); // set socket to best one if no socket specified
if (socket == null) {
dbg("no sockets");
if (typeof opts.cb === "function") {
opts.cb("no hubs currently connected to this project");
}
return;
}
if (opts.cb != null) {
if (opts.timeout) {
dbg("configure timeout");
fail = () => {
dbg("failed");
delete this._hub_callbacks[opts.message.id];
if (typeof opts.cb === "function") {
opts.cb(`timeout after ${opts.timeout}s`);
}
return delete opts.cb;
};
timer = setTimeout(fail, opts.timeout * 1000);
}
if ((base = opts.message).id == null) {
base.id = misc.uuid();
}
cb = this._hub_callbacks[opts.message.id] = (resp) => {
//dbg("got response: #{misc.trunc(json(resp),400)}")
if (timer != null) {
clearTimeout(timer);
timer = void 0;
}
if (resp.event === 'error') {
return typeof opts.cb === "function" ? opts.cb(resp.error ? resp.error : 'error') : void 0;
} else {
return typeof opts.cb === "function" ? opts.cb(void 0, resp) : void 0;
}
};
this._hub_client_sockets[socket.id].callbacks[opts.message.id] = cb;
}
// Finally, send the message
return socket.write_mesg('json', opts.message);
}
query(opts) {
var mesg, socket;
boundMethodCheck(this, ref);
opts = defaults(opts, {
query: required, // a query (see schema.coffee)
changes: void 0, // whether or not to create a changefeed
options: void 0, // options to the query, e.g., [{limit:5}] )
standby: false, // **IGNORED**
timeout: 30, // how long to wait for initial result
cb: required
});
if ((opts.options != null) && !misc.is_array(opts.options)) {
throw Error("options must be an array");
return;
}
mesg = message.query({
id: misc.uuid(),
query: opts.query,
options: opts.options,
changes: opts.changes,
multi_response: opts.changes
});
socket = this.get_hub_socket();
if (socket == null) {
// It will try later when one is available...
opts.cb("no hub socket available");
return;
}
if (opts.changes) {
// Record socket for this changefeed in @_changefeed_sockets
this._changefeed_sockets[mesg.id] = socket;
// CRITICAL: On error or end, send an end error to the synctable, so that it will
// attempt to reconnect (and also stop writing to the socket).
// This is important, since for project clients
// the disconnected event is only emitted when *all* connections from
// hubs to the local_hub end. If two connections s1 and s2 are open,
// and s1 is used for a sync table, and s1 closes (e.g., hub1 is restarted),
// then s2 is still open and no 'disconnected' event is emitted. Nonetheless,
// it's important for the project to consider the synctable broken and
// try to reconnect it, which in this case it would do using s2.
socket.on('error', () => {
return opts.cb('socket-end');
});
socket.on('end', () => {
return opts.cb('socket-end');
});
}
return this.call({
message: mesg,
timeout: opts.timeout,
socket: socket,
cb: opts.cb
});
}
_query_cancel(opts) {
var socket;
boundMethodCheck(this, ref);
opts = defaults(opts, {
id: required, // changefeed id
cb: void 0
});
socket = this._changefeed_sockets[opts.id];
if (socket == null) {
return typeof opts.cb === "function" ? opts.cb() : void 0;
} else {
return this.call({
message: message.query_cancel({
id: opts.id
}),
timeout: 30,
socket: socket,
cb: opts.cb
});
}
}
async query_cancel(id) {
boundMethodCheck(this, ref);
return (await callback2(this._query_cancel, {
id: id
}));
}
sync_table(query, options, throttle_changes = void 0) {
boundMethodCheck(this, ref);
return synctable2.synctable(query, options, this, throttle_changes);
}
async synctable_project(project_id, query, options) {
var the_synctable;
boundMethodCheck(this, ref);
// TODO: this is ONLY for syncstring tables (syncstrings, patches, cursors).
// Also, options are ignored -- since we use whatever was selected by the frontend.
the_synctable = (await require('./sync/open-synctables').get_synctable(query, this));
// To provide same API, must also wait until done initializing.
if (the_synctable.get_state() !== 'connected') {
await once(the_synctable, 'connected');
}
if (the_synctable.get_state() !== 'connected') {
throw Error("Bug -- state of synctable must be connected " + JSON.stringify(query));
}
return the_synctable;
}
syncdoc(opts) {
boundMethodCheck(this, ref);
opts = defaults(opts, {
path: required
});
return get_syncdoc(opts.path);
}
symmetric_channel(name) {
boundMethodCheck(this, ref);
return require('./browser-websocket/symmetric_channel').symmetric_channel(name);
}
async write_file(opts) {
var dbg, err, now, path, ref1;
boundMethodCheck(this, ref);
opts = defaults(opts, {
path: required,
data: required,
cb: required
});
path = join(process.env.HOME, opts.path);
if (this._file_io_lock == null) {
this._file_io_lock = {};
}
dbg = this.dbg(`write_file(path='${opts.path}')`);
dbg();
now = new Date();
if (now - ((ref1 = this._file_io_lock[path]) != null ? ref1 : 0) < 15000) { // lock automatically expires after 15 seconds (see https://github.com/sagemathinc/cocalc/issues/1147)
dbg("LOCK");
// Try again in about 1s.
setTimeout((() => {
return this.write_file(opts);
}), 500 + 500 * Math.random());
return;
}
this._file_io_lock[path] = now;
dbg(` = ${misc.to_json(this._file_io_lock)}`);
try {
await ensureContainingDirectoryExists(path);
await writeFile(path, opts.data);
dbg("success");
return opts.cb();
} catch (error) {
err = error;
dbg(`error -- ${err}`);
return opts.cb(err);
} finally {
delete this._file_io_lock[path];
}
}
path_read(opts) {
var content, dbg, now, path, ref1;
boundMethodCheck(this, ref);
opts = defaults(opts, {
path: required,
maxsize_MB: void 0, // in megabytes; if given and file would be larger than this, then cb(err)
cb: required // cb(err, file content as string (not Buffer!))
});
content = void 0;
path = join(process.env.HOME, opts.path);
dbg = this.dbg(`path_read(path='${opts.path}', maxsize_MB=${opts.maxsize_MB})`);
dbg();
if (this._file_io_lock == null) {
this._file_io_lock = {};
}
now = new Date();
if (now - ((ref1 = this._file_io_lock[path]) != null ? ref1 : 0) < 15000) { // lock expires after 15 seconds (see https://github.com/sagemathinc/cocalc/issues/1147)
dbg("LOCK");
// Try again in 1s.
setTimeout((() => {
return this.path_read(opts);
}), 500 + 500 * Math.random());
return;
}
this._file_io_lock[path] = now;
dbg(` = ${misc.to_json(this._file_io_lock)}`);
return async.series([
(cb) => {
if (opts.maxsize_MB != null) {
dbg("check if file too big");
return this.file_size({
filename: opts.path,
cb: (err,
size) => {
if (err) {
dbg(`error checking -- ${err}`);
return cb(err);
} else if (size > opts.maxsize_MB * 1000000) {
dbg("file is too big!");
return cb(`file '${opts.path}' size (=${size / 1000000}MB) too large (must be at most ${opts.maxsize_MB}MB); try opening it in a Terminal with vim instead or click Help in the upper right to open a support request`);
} else {
dbg("file is fine");
return cb();
}
}
});
} else {
return cb();
}
},
(cb) => {
return fs.readFile(path,
(err,
data) => {
if (err) {
dbg(`error reading file -- ${err}`);
return cb(err);
} else {
dbg('read file');
content = data.toString();
return cb();
}
});
}
], (err) => {
delete this._file_io_lock[path];
return opts.cb(err, content);
});
}
path_access(opts) {
var access, i, len, ref1, s;
boundMethodCheck(this, ref);
opts = defaults(opts, {
path: required, // string
mode: required, // string -- sub-sequence of 'rwxf' -- see https://nodejs.org/api/fs.html#fs_class_fs_stats
cb: required // cb(err); err = if any access fails; err=undefined if all access is OK
});
access = 0;
ref1 = opts.mode;
for (i = 0, len = ref1.length; i < len; i++) {
s = ref1[i];
access |= fs[s.toUpperCase() + '_OK'];
}
return fs.access(opts.path, access, opts.cb);
}
path_exists(opts) {
var dbg;
boundMethodCheck(this, ref);
opts = defaults(opts, {
path: required,
cb: required
});
dbg = this.dbg(`checking if path (='${opts.path}') exists`);
dbg();
return fs.exists(opts.path, (exists) => {
dbg(`returned ${exists}`);
return opts.cb(void 0, exists); // err actually never happens with node.js, so we change api to be more consistent
});
}
path_stat(opts) { // see https://nodejs.org/api/fs.html#fs_class_fs_stats
boundMethodCheck(this, ref);
opts = defaults(opts, {
path: required,
cb: required
});
return fs.stat(opts.path, opts.cb);
}
file_size(opts) {
boundMethodCheck(this, ref);
opts = defaults(opts, {
filename: required,
cb: required
});
return this.path_stat({
path: opts.filename,
cb: (err, stat) => {
return opts.cb(err, stat != null ? stat.size : void 0);
}
});
}
shell(opts) {
boundMethodCheck(this, ref);
return misc_node.execute_code(opts);
}
sage_session(opts) {
boundMethodCheck(this, ref);
opts = defaults(opts, {
path: required
});
return sage_session.sage_session({
path: opts.path,
client: this
});
}
jupyter_kernel(opts) {
boundMethodCheck(this, ref);
opts.client = this;
return jupyter.kernel(opts);
}
async jupyter_kernel_info() {
boundMethodCheck(this, ref);
return (await get_kernel_data());
}
watch_file(opts) {
var dbg, path;
boundMethodCheck(this, ref);
opts = defaults(opts, {
path: required,
interval: 1500, // polling interval in ms
debounce: 500 // don't fire until at least this many ms after the file has REMAINED UNCHANGED
});
path = require('path').join(process.env.HOME, opts.path);
dbg = this.dbg(`watch_file(path='${path}')`);
dbg(`watching file '${path}'`);
return new Watcher(path, opts.interval, opts.debounce);
}
save_blob(opts) {
var dbg, hub, uuid;
boundMethodCheck(this, ref);
opts = defaults(opts, {
blob: required, // Buffer of data
sha1: void 0,
uuid: void 0, // if given then uuid must be derived from sha1 hash
cb: void 0 // (err, resp)
});
if (opts.uuid != null) {
uuid = opts.uuid;
} else {
uuid = misc_node.uuidsha1(opts.blob, opts.sha1);
}
dbg = this.dbg(`save_blob(uuid='${uuid}')`);
hub = this.get_hub_socket();
if (hub == null) {
dbg("fail -- no global hubs");
if (typeof opts.cb === "function") {
opts.cb('no global hubs are connected to the local hub, so nowhere to send file');
}
return;
}
dbg("sending blob mesg");
hub.write_mesg('blob', {
uuid: uuid,
blob: opts.blob
});
dbg("waiting for response");
return blobs.receive_save_blob_message({
sha1: uuid,
cb: (resp) => {
if (resp != null ? resp.error : void 0) {
dbg(`fail -- '${resp.error}'`);
return typeof opts.cb === "function" ? opts.cb(resp.error, resp) : void 0;
} else {
dbg("success");
return typeof opts.cb === "function" ? opts.cb(void 0, resp) : void 0;
}
}
});
}
get_blob(opts) {
var dbg;
boundMethodCheck(this, ref);
opts = defaults(opts, {
blob: required, // Buffer of data
sha1: void 0,
uuid: void 0, // if given is uuid derived from sha1
cb: void 0 // (err, resp)
});
dbg = this.dbg("get_blob");
dbg(opts.sha1);
return typeof opts.cb === "function" ? opts.cb('get_blob: not implemented') : void 0;
}
touch_project(project_id) {
boundMethodCheck(this, ref);
}
async get_syncdoc_history(string_id, patches = false) {
var dbg, mesg;
boundMethodCheck(this, ref);
dbg = this.dbg("get_syncdoc_history");
dbg(string_id, patches);
mesg = message.get_syncdoc_history({
string_id: string_id,
patches: patches
});
return (await callback2(this.call, {
message: mesg
}));
}
is_deleted(filename, project_id) { // project_id is ignored, of course
var listings;
boundMethodCheck(this, ref);
try {
listings = get_listings_table();
return listings.is_deleted(filename);
} catch (error) {
// is_deleted can raise an exception if the table is
// not yet initialized, in which case we fall back
// to actually looking. We have to use existsSync
// because is_deleted is not an async function.
return !fs.existsSync(join(process.env.HOME, filename));
}
}
async set_deleted(filename, project_id) { // project_id is ignored
var listings;
boundMethodCheck(this, ref);
listings = get_listings_table();
return (await listings.set_deleted(filename));
}
};
}).call(this);
//# sourceMappingURL=client.js.map