(core) Configure more comprehensive eslint rules for Typescript

Summary:
- Update rules to be more like we've had with tslint
- Switch tsserver plugin to eslint (tsserver makes for a much faster way to lint in editors)
- Apply suggested auto-fixes
- Fix all lint errors and warnings in core/, app/, test/

Test Plan: Some behavior may change subtly (e.g. added missing awaits), relying on existing tests to catch problems.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2785
pull/21/head
Dmitry S 3 years ago
parent 91fdef58ac
commit 526b0ad33e

@ -241,9 +241,9 @@ AceEditor.prototype._getContentHeight = function() {
let _RangeConstructor = null; //singleton, load it lazily
AceEditor.makeRange = function(a,b,c,d) {
AceEditor.makeRange = function(a, b, c, d) {
_RangeConstructor = _RangeConstructor || ace.acequire('ace/range').Range;
return new _RangeConstructor(a,b,c,d);
return new _RangeConstructor(a, b, c, d);
};
module.exports = AceEditor;

@ -262,7 +262,7 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
if (this._loaded || !this._gristDoc) { return; }
this._loading(true);
// Returned actions are ordered with earliest actions first.
const result = await this._gristDoc!.docComm.getActionSummaries();
const result = await this._gristDoc.docComm.getActionSummaries();
this._loading(false);
this._loaded = true;
// Add the actions to our action log.

@ -15,7 +15,7 @@ import * as ko from 'knockout';
import noop = require('lodash/noop');
// To simplify diff (avoid rearranging methods to satisfy private/public order).
// tslint:disable:member-ordering
/* eslint-disable @typescript-eslint/member-ordering */
type AceEditor = any;
@ -130,7 +130,7 @@ export class ColumnTransform extends Disposable {
this.transformColumn = this.field.column();
this.transformColumn.origColRef(this.origColumn.getRowId());
this._setTransforming(true);
return await this.postAddTransformColumn();
return this.postAddTransformColumn();
} finally {
this.isCallPending(false);
}
@ -167,7 +167,7 @@ export class ColumnTransform extends Disposable {
/**
* A derived class can override to do some processing after this.transformColumn has been set.
*/
protected postAddTransformColumn() {
protected postAddTransformColumn(): void {
// Nothing in base class.
}
@ -207,7 +207,7 @@ export class ColumnTransform extends Disposable {
} finally {
// Wait until the change completed to set column back, to avoid value flickering.
field.colRef(origRef);
tableData.sendTableAction(['RemoveColumn', transformColId]);
void tableData.sendTableAction(['RemoveColumn', transformColId]);
this.dispose();
}
}

@ -350,11 +350,11 @@ GridView.prototype.fillSelectionDown = function() {
}).filter(colId => colId);
var colInfo = _.object(colIds, colIds.map(colId => {
var val = this.tableModel.tableData.getValue(rowIds[0],colId);
var val = this.tableModel.tableData.getValue(rowIds[0], colId);
return rowIds.map(() => val);
}));
this.tableModel.sendTableAction(["BulkUpdateRecord",rowIds,colInfo]);
this.tableModel.sendTableAction(["BulkUpdateRecord", rowIds, colInfo]);
};

@ -153,7 +153,7 @@ export class Importer extends Disposable {
} else if (item.kind === "url") {
uploadResult = await fetchURL(this._docComm, item.url);
} else {
throw new Error(`Import source of kind ${item!.kind} are not yet supported!`);
throw new Error(`Import source of kind ${(item as any).kind} are not yet supported!`);
}
}
}
@ -391,7 +391,7 @@ export class Importer extends Disposable {
}
function getSourceDescription(sourceInfo: SourceInfo, upload: UploadResult) {
const origName = upload!.files[sourceInfo.uploadFileIndex].origName;
const origName = upload.files[sourceInfo.uploadFileIndex].origName;
return sourceInfo.origTableName ? origName + ' - ' + sourceInfo.origTableName : origName;
}

@ -25,8 +25,8 @@ export function makeSearchToolbarGroup(gristDoc: GristDoc) {
// Active normally.
const commandGroup = createGroup({
find: () => { input.focus(); },
findNext: () => { searcher.findNext(); }, // tslint:disable-line:no-floating-promises TODO
findPrev: () => { searcher.findPrev(); }, // tslint:disable-line:no-floating-promises TODO
findNext: () => { searcher.findNext(); }, // eslint-disable-line @typescript-eslint/no-floating-promises
findPrev: () => { searcher.findPrev(); }, // eslint-disable-line @typescript-eslint/no-floating-promises
}, null, true);
// Return an array of one item (for a toolbar group of a single item). The item is an array of
@ -49,7 +49,7 @@ export function makeSearchToolbarGroup(gristDoc: GristDoc) {
// the searchbox is created so early that the actions like accept/cancel get overridden).
dom.on('keydown', (e: KeyboardEvent) => {
switch (e.keyCode) {
case 13: searcher.findNext(); break; // tslint:disable-line:no-floating-promises TODO
case 13: searcher.findNext(); break; // eslint-disable-line @typescript-eslint/no-floating-promises
case 27: input.blur(); break;
}
})

@ -264,7 +264,7 @@ CellSelector.prototype.rowCount = function() {
return this.rowUpper() - this.rowLower() + 1;
};
CellSelector.prototype.selectArea = function(rowStartIdx,colStartIdx,rowEndIdx,colEndIdx) {
CellSelector.prototype.selectArea = function(rowStartIdx, colStartIdx, rowEndIdx, colEndIdx) {
this.row.start(rowStartIdx);
this.col.start(colStartIdx);
this.row.end(rowEndIdx);

@ -22,7 +22,7 @@ import isEmpty = require('lodash/isEmpty');
import pickBy = require('lodash/pickBy');
// To simplify diff (avoid rearranging methods to satisfy private/public order).
// tslint:disable:member-ordering
/* eslint-disable @typescript-eslint/member-ordering */
/**
* Creates an instance of TypeTransform for a single field. Extends ColumnTransform.

@ -58,7 +58,7 @@ function ViewConfigTab(options) {
}) || self.viewSectionData.at(0);
}));
this.isDetail = this.autoDispose(ko.computed(function() {
return ['detail','single'].includes(this.viewModel.activeSection().parentKey());
return ['detail', 'single'].includes(this.viewModel.activeSection().parentKey());
}, this));
this.isChart = this.autoDispose(ko.computed(function() {
return this.viewModel.activeSection().parentKey() === 'chart';}, this));

@ -35,7 +35,6 @@ declare module "app/client/components/BaseView" {
import {Cursor, CursorPos} from 'app/client/components/Cursor';
import {GristDoc} from 'app/client/components/GristDoc';
import {Disposable} from 'app/client/lib/dispose';
import {KoArray} from "app/client/lib/koArray";
import * as BaseRowModel from "app/client/models/BaseRowModel";
import {DataRowModel} from 'app/client/models/DataRowModel';
import {LazyArrayModel} from "app/client/models/DataTableModel";
@ -72,10 +71,8 @@ declare module "app/client/components/BaseView" {
}
declare module "app/client/components/RefSelect" {
import {GristDoc, TabContent} from 'app/client/components/GristDoc';
import {Disposable} from 'app/client/lib/dispose';
import {ColumnRec} from "app/client/models/DocModel";
import {DomArg} from 'grainjs';
import {DocModel} from "app/client/models/DocModel";
import {FieldBuilder} from "app/client/widgets/FieldBuilder";
@ -161,11 +158,11 @@ declare module "app/client/models/BaseRowModel" {
class BaseRowModel extends Disposable {
public id: ko.Computed<number>;
public _index: ko.Observable<number|null>;
public getRowId(): number;
public updateColValues(colValues: ColValues): Promise<void>;
public _table: TableModel;
protected _rowId: number | 'new' | null;
protected _fields: string[];
public getRowId(): number;
public updateColValues(colValues: ColValues): Promise<void>;
}
export = BaseRowModel;
}
@ -286,10 +283,9 @@ declare module "app/client/models/DataTableModel" {
import * as BaseRowModel from "app/client/models/BaseRowModel";
import {DocModel, TableRec} from "app/client/models/DocModel";
import {TableQuerySets} from 'app/client/models/QuerySet';
import {RowSource, SortedRowSet} from "app/client/models/rowset";
import {SortedRowSet} from "app/client/models/rowset";
import {TableData} from "app/client/models/TableData";
import * as TableModel from "app/client/models/TableModel";
import {CellValue} from "app/common/DocActions";
namespace DataTableModel {
interface LazyArrayModel<T> extends KoArray<T | null> {

@ -1,7 +1,7 @@
/**
* Implements an autocomplete dropdown.
*/
import {createPopper, Instance as Popper, Modifier, Options as PopperOptions} from '@popperjs/core';
import {createPopper, Modifier, Instance as Popper, Options as PopperOptions} from '@popperjs/core';
import {ACItem, ACResults, HighlightFunc} from 'app/client/lib/ACIndex';
import {reportError} from 'app/client/models/errors';
import {Disposable, dom, EventCB, IDisposable} from 'grainjs';

@ -174,7 +174,7 @@ export class BillingModelImpl extends Disposable implements BillingModel {
await this._billingAPI.updateAddress(newAddr || undefined, newSettings || undefined);
}
// If there is an org update, re-initialize the org in the client.
if (newSettings) { await this._appModel.topAppModel.initialize(); }
if (newSettings) { this._appModel.topAppModel.initialize(); }
} else {
throw new Error('BillingPage _submit error: no task in progress');
}

@ -27,13 +27,6 @@ const ROW_ID_SKIP = -1;
* This should be the only part of the code that knows that.
*/
export class ExtraRows {
readonly leftTableDelta?: TableDelta;
readonly rightTableDelta?: TableDelta;
readonly rightAddRows: Set<number>;
readonly rightRemoveRows: Set<number>;
readonly leftAddRows: Set<number>;
readonly leftRemoveRows: Set<number>;
/**
* Map back from a possibly synthetic row id to an original strictly-positive row id.
*/
@ -44,7 +37,14 @@ export class ExtraRows {
return { type: 'local-remove', id: -(rowId + 2) / 2 };
}
public constructor(readonly tableId: string, readonly comparison?: DocStateComparisonDetails) {
public readonly leftTableDelta?: TableDelta;
public readonly rightTableDelta?: TableDelta;
public readonly rightAddRows: Set<number>;
public readonly rightRemoveRows: Set<number>;
public readonly leftAddRows: Set<number>;
public readonly leftRemoveRows: Set<number>;
public constructor(public readonly tableId: string, public readonly comparison?: DocStateComparisonDetails) {
const remoteTableId = getRemoteTableId(tableId, comparison);
this.leftTableDelta = this.comparison?.leftChanges?.tableDeltas[tableId];
if (remoteTableId) {

@ -100,7 +100,7 @@ export class DocData extends BaseDocData {
this._nextDesc = options.description;
this._lastActionNum = null;
this._triggerBundleFinalize = triggerFinalize;
await prepareResolve(options.prepare());
prepareResolve(options.prepare());
this._shouldIncludeInBundle = options.shouldIncludeInBundle;
await triggerFinalizePromise;
@ -162,7 +162,7 @@ export class DocData extends BaseDocData {
public sendActions(actions: UserAction[], optDesc?: string): Promise<any[]> {
// Some old code relies on this promise being a bluebird Promise.
// TODO Remove bluebird and this cast.
return bluebird.Promise.resolve(this._sendActionsImpl(actions, optDesc)) as any;
return bluebird.Promise.resolve(this._sendActionsImpl(actions, optDesc)) as unknown as Promise<any[]>;
}
/**

@ -94,7 +94,8 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
public readonly isReadonly = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isReadonly : false);
public readonly isPrefork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isPreFork : false);
public readonly isFork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isFork : false);
public readonly isRecoveryMode = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isRecoveryMode : false);
public readonly isRecoveryMode = Computed.create(this, this.currentDoc,
(use, doc) => doc ? doc.isRecoveryMode : false);
public readonly userOverride = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.userOverride : null);
public readonly isBareFork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isBareFork : false);
public readonly isSample = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isSample : false);
@ -133,8 +134,9 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
if (!urlId) {
this._openerHolder.clear();
} else {
FlowRunner.create(this._openerHolder, (flow: AsyncFlow) => this._openDoc(flow, urlId, urlOpenMode,
state.params?.compare, linkParameters))
FlowRunner.create(this._openerHolder,
(flow: AsyncFlow) => this._openDoc(flow, urlId, urlOpenMode, state.params?.compare, linkParameters)
)
.resultPromise.catch(err => this._onOpenError(err));
}
}
@ -305,8 +307,10 @@ function addMenu(importSources: ImportSource[], gristDoc: GristDoc, isReadonly:
menuIcon("Widget"), "Add Widget to Page", testId('dp-add-widget-to-page'),
dom.cls('disabled', isReadonly)
),
menuItem(() => gristDoc.addEmptyTable().catch(reportError), menuIcon("TypeTable"), "Add Empty Table", testId('dp-empty-table'),
dom.cls('disabled', isReadonly)),
menuItem(() => gristDoc.addEmptyTable().catch(reportError),
menuIcon("TypeTable"), "Add Empty Table", testId('dp-empty-table'),
dom.cls('disabled', isReadonly)
),
menuDivider(),
...importSources.map((importSource, i) =>
menuItem(importSource.action,

@ -314,7 +314,7 @@ function convertQueryToRefs(docModel: DocModel, query: Query): QueryRefs {
const tableRec: any = docModel.dataTables[query.tableId].tableMetaRow;
const colRefsByColId: {[colId: string]: number} = {};
for (const col of (tableRec as any).columns.peek().peek()) {
for (const col of tableRec.columns.peek().peek()) {
colRefsByColId[col.colId.peek()] = col.getRowId();
}

@ -84,7 +84,7 @@ export class SearchModelImpl extends Disposable implements SearchModel {
// Listen to input value changes (debounced) to activate searching.
const findFirst = debounce((_value: string) => this._findFirst(_value), 100);
this.autoDispose(this.value.addListener(v => { findFirst(v); }));
this.autoDispose(this.value.addListener(v => { void findFirst(v); }));
}
public async findNext() {

@ -7,7 +7,7 @@ import {DocData} from 'app/client/models/DocData';
import {DocAction, ReplaceTableData, TableDataAction, UserAction} from 'app/common/DocActions';
import {isRaisedException} from 'app/common/gristTypes';
import {countIf} from 'app/common/gutil';
import {ColTypeMap, TableData as BaseTableData} from 'app/common/TableData';
import {TableData as BaseTableData, ColTypeMap} from 'app/common/TableData';
import {BaseFormatter} from 'app/common/ValueFormatter';
import {Emitter} from 'grainjs';

@ -153,7 +153,7 @@ class BillingPaymentForm extends BillingSubForm {
}) {
super();
const autofill = this._options.autofill;
const stripeAPIKey = (G.window as any).gristConfig.stripeAPIKey;
const stripeAPIKey = G.window.gristConfig.stripeAPIKey;
try {
this._stripe = G.Stripe(stripeAPIKey);
this._elements = this._stripe.elements();
@ -462,7 +462,7 @@ function checkRequired(propertyName: string) {
// if the current observable value is valid.
function createValidated(
owner: IDisposableOwnerT<any>,
checkValidity: (value: string) => void
checkValidity: (value: string) => void|Promise<void>,
): IValidated<string> {
const value = Observable.create(owner, '');
const isInvalid = Observable.create<boolean>(owner, false);

@ -352,7 +352,7 @@ function getFieldNewPosition(fields: KoArray<ViewFieldRec>, item: IField,
return tableUtil.fieldInsertPositions(fields, index, 1)[0];
}
function getItemIndex<T>(collection: KoArray<ViewFieldRec>, item: ViewFieldRec|null): number {
function getItemIndex(collection: KoArray<ViewFieldRec>, item: ViewFieldRec|null): number {
if (item !== null) {
return collection.peek().indexOf(item);
}

@ -194,8 +194,8 @@ export const cssHideForNarrowScreen = styled('div', `
* Attaches the global css properties to the document's root to them available in the page.
*/
export function attachCssRootVars(productFlavor: ProductFlavor, varsOnly: boolean = false) {
dom.update(document.documentElement!, varsOnly ? dom.cls(cssVarsOnly.className) : dom.cls(cssRootVars));
document.documentElement!.classList.add(cssRoot.className);
dom.update(document.documentElement, varsOnly ? dom.cls(cssVarsOnly.className) : dom.cls(cssRootVars));
document.documentElement.classList.add(cssRoot.className);
document.body.classList.add(cssBody.className);
const theme = getTheme(productFlavor);
if (theme.bodyClassName) {

@ -4,7 +4,7 @@ import { CellValue } from 'app/common/DocActions';
import { isVersions } from 'app/common/gristTypes';
import { inlineStyle } from 'app/common/gutil';
import { BaseFormatter } from 'app/common/ValueFormatter';
import { Diff, DIFF_DELETE, DIFF_INSERT, diff_match_patch as DiffMatchPatch, DIFF_EQUAL } from 'diff-match-patch';
import { Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch as DiffMatchPatch } from 'diff-match-patch';
import { Computed, dom } from 'grainjs';
/**

@ -27,7 +27,7 @@ import * as gristTypes from 'app/common/gristTypes';
import * as gutil from 'app/common/gutil';
import { CellValue } from 'app/plugin/GristData';
import { delay } from 'bluebird';
import { Computed, Disposable, dom as grainjsDom, fromKo, Holder, IDisposable, makeTestId } from 'grainjs';
import { Computed, Disposable, fromKo, dom as grainjsDom, Holder, IDisposable, makeTestId } from 'grainjs';
import * as ko from 'knockout';
import * as _ from 'underscore';
@ -77,7 +77,7 @@ export class FieldBuilder extends Disposable {
private readonly widgetCons: ko.Computed<{create: (...args: any[]) => NewAbstractWidget}>;
private readonly docModel: DocModel;
public constructor(readonly gristDoc: GristDoc, readonly field: ViewFieldRec,
public constructor(public readonly gristDoc: GristDoc, public readonly field: ViewFieldRec,
private _cursor: Cursor) {
super();

@ -315,7 +315,7 @@ function readAclRules(docData: DocData, {log, compile}: ReadAclOptions): ReadAcl
}
for (const [resourceId, rules] of rulesByResource.entries()) {
const resourceRec = resourcesTable.getRecord(resourceId as number);
const resourceRec = resourcesTable.getRecord(resourceId);
if (!resourceRec) {
throw new Error(`ACLRule ${rules[0].id} refers to an invalid ACLResource ${resourceId}`);
continue;

@ -82,7 +82,8 @@ const SCHEMA_ACTIONS = new Set(['AddTable', 'RemoveTable', 'RenameTable', 'AddCo
/**
* Determines whether a given action is a schema action or not.
*/
export function isSchemaAction(action: DocAction): action is AddTable | RemoveTable | RenameTable | AddColumn | RemoveColumn | RenameColumn | ModifyColumn {
export function isSchemaAction(action: DocAction):
action is AddTable | RemoveTable | RenameTable | AddColumn | RemoveColumn | RenameColumn | ModifyColumn {
return SCHEMA_ACTIONS.has(action[0]);
}

@ -48,7 +48,7 @@ export interface OpenLocalDocResult {
userOverride?: UserOverride;
}
export class UserOverride {
export interface UserOverride {
user: FullUser|null;
access: Role|null;
}

@ -76,7 +76,7 @@ export abstract class BaseComponent implements IForwarderDest {
public async forwardMessage(msg: IMsgCustom): Promise<any> {
if (!this._activated) { await this.activate(); }
this.inactivityTimer.ping();
this.doForwardMessage(msg); // tslint:disable-line:no-floating-promises TODO
this.doForwardMessage(msg); // eslint-disable-line @typescript-eslint/no-floating-promises
}
protected abstract doForwardCall(c: IMsgRpcCall): Promise<any>;

@ -629,7 +629,7 @@ export class UserAPIImpl extends BaseAPI implements UserAPI {
}
export class DocWorkerAPIImpl extends BaseAPI implements DocWorkerAPI {
constructor(readonly url: string, _options: IOptions = {}) {
constructor(public readonly url: string, _options: IOptions = {}) {
super(_options);
}
@ -682,7 +682,7 @@ export class DocWorkerAPIImpl extends BaseAPI implements DocWorkerAPI {
export class DocAPIImpl extends BaseAPI implements DocAPI {
private _url: string;
constructor(url: string, readonly docId: string, options: IOptions = {}) {
constructor(url: string, public readonly docId: string, options: IOptions = {}) {
super(options);
this._url = `${url}/api/docs/${docId}`;
}

@ -8,7 +8,9 @@
export function tbind<T, R, Args extends any[]>(func: (this: T, ...a: Args) => R, context: T): (...a: Args) => R;
// Bind context and first arg for a function of up to 5 args.
export function tbind<T, R, X, Args extends any[]>(func: (this: T, x: X, ...a: Args) => R, context: T, x: X): (...a: Args) => R;
export function tbind<T, R, X, Args extends any[]>(
func: (this: T, x: X, ...a: Args) => R, context: T, x: X
): (...a: Args) => R;
export function tbind(func: any, context: any, ...boundArgs: any[]): any {
return func.bind(context, ...boundArgs);

@ -12,7 +12,8 @@ type PromisifiedFunction<T extends AnyFunction> =
T extends (a1: infer A1) => infer U ? (a1: A1) => Promise<Unpacked<U>> :
T extends (a1: infer A1, a2: infer A2) => infer U ? (a1: A1, a2: A2) => Promise<Unpacked<U>> :
T extends (a1: infer A1, a2: infer A2, a3: infer A3) => infer U ? (a1: A1, a2: A2, a3: A3) => Promise<Unpacked<U>> :
T extends (a1: infer A1, a2: infer A2, a3: infer A3, a4: infer A4) => infer U ? (a1: A1, a2: A2, a3: A3, a4: A4) => Promise<Unpacked<U>> :
T extends (a1: infer A1, a2: infer A2, a3: infer A3, a4: infer A4) =>
infer U ? (a1: A1, a2: A2, a3: A3, a4: A4) => Promise<Unpacked<U>> :
// ...
T extends (...args: any[]) => infer U ? (...args: any[]) => Promise<Unpacked<U>> : T;

@ -55,7 +55,9 @@ export class DocApiForwarder {
app.use('^/api/docs$', withoutDoc);
}
private async _forwardToDocWorker(withDocId: boolean, role: 'viewers'|null, req: express.Request, res: express.Response): Promise<void> {
private async _forwardToDocWorker(
withDocId: boolean, role: 'viewers'|null, req: express.Request, res: express.Response,
): Promise<void> {
let docId: string|null = null;
if (withDocId) {
const docAuth = await getOrSetDocAuth(req as RequestWithLogin, this._dbManager, req.params.docId);

@ -424,7 +424,7 @@ export class DocWorkerMap implements IDocWorkerMap {
}
public async updateDocStatus(docId: string, checksum: string): Promise<void> {
this.updateChecksum('doc', docId, checksum);
return this.updateChecksum('doc', docId, checksum);
}
public async updateChecksum(family: string, key: string, checksum: string) {

@ -8,8 +8,8 @@ import {checkSubdomainValidity} from 'app/common/orgNameUtils';
import * as roles from 'app/common/roles';
// TODO: API should implement UserAPI
import {ANONYMOUS_USER_EMAIL, DocumentProperties, EVERYONE_EMAIL,
ManagerDelta, NEW_DOCUMENT_CODE, Organization as OrgInfo,
OrganizationProperties, PermissionData, PermissionDelta, SUPPORT_EMAIL, UserAccessData,
ManagerDelta, NEW_DOCUMENT_CODE, OrganizationProperties,
Organization as OrgInfo, PermissionData, PermissionDelta, SUPPORT_EMAIL, UserAccessData,
WorkspaceProperties} from "app/common/UserAPI";
import {AclRule, AclRuleDoc, AclRuleOrg, AclRuleWs} from "app/gen-server/entity/AclRule";
import {Alias} from "app/gen-server/entity/Alias";
@ -1903,7 +1903,7 @@ export class HomeDBManager extends EventEmitter {
name: u.name,
email: u.logins.map((login: Login) => login.displayEmail)[0],
picture: u.picture,
access: userRoleMap[u.id] as roles.Role
access: userRoleMap[u.id]
}));
return {
status: 200,
@ -2811,7 +2811,7 @@ export class HomeDBManager extends EventEmitter {
let effectiveUserId = userId;
let threshold = options.markPermissions;
if (options.allowSpecialPermit && scope.specialPermit && scope.specialPermit.docId) {
query = query.andWhere('docs.id = :docId', {docId: scope.specialPermit.docId!});
query = query.andWhere('docs.id = :docId', {docId: scope.specialPermit.docId});
effectiveUserId = this.getPreviewerUserId();
threshold = Permissions.VIEW;
}
@ -3051,7 +3051,7 @@ export class HomeDBManager extends EventEmitter {
if (!groupProps.orgOnly || !inherit) {
// Skip this group if it's an org only group and the resource inherits from a parent.
const group = new Group();
group.name = groupProps.name as roles.Role;
group.name = groupProps.name;
if (inherit) {
this._setInheritance(group, inherit);
}
@ -3333,7 +3333,10 @@ export class HomeDBManager extends EventEmitter {
`${everyoneId} IN (gu0.user_id, gu1.user_id, gu2.user_id, gu3.user_id) ` +
`then ${everyoneContribution} else (case when ` +
`${anonId} IN (gu0.user_id, gu1.user_id, gu2.user_id, gu3.user_id) ` +
`then ${Permissions.PUBLIC} | acl_rules.permissions else acl_rules.permissions end) end)`, 8), 'permissions');
`then ${Permissions.PUBLIC} | acl_rules.permissions else acl_rules.permissions end) end)`, 8
),
'permissions'
);
}
q = q.from('acl_rules', 'acl_rules');
q = this._getUsersAcls(q, users, accessStyle);

@ -210,7 +210,9 @@ export class Housekeeper {
// Call a document endpoint with a permit, cleaning up after the call.
// Checks that the user is the support user.
private _withSupport(callback: (docId: string, headers: Record<string, string>) => Promise<Fetch.Response>): express.RequestHandler {
private _withSupport(
callback: (docId: string, headers: Record<string, string>) => Promise<Fetch.Response>
): express.RequestHandler {
return expressWrap(async (req, res) => {
const userId = getAuthorizedUserId(req);
if (userId !== this._dbManager.getSupportUserId()) {

@ -133,7 +133,7 @@ export async function addImporter(name: string, path: string, mode: 'fullscreen'
*/
export function ready(): void {
rpc.processIncoming();
rpc.sendReadyMessage();
void rpc.sendReadyMessage();
}
function getPluginPath(location: Location) {

@ -715,7 +715,7 @@ export class ActiveDoc extends EventEmitter {
*/
public async getFormulaError(docSession: DocSession, tableId: string, colId: string,
rowId: number): Promise<CellValue> {
if (!this._granularAccess.hasTableAccess(docSession, tableId)) { return null; }
if (!await this._granularAccess.hasTableAccess(docSession, tableId)) { return null; }
this.logInfo(docSession, "getFormulaError(%s, %s, %s, %s)",
docSession, tableId, colId, rowId);
await this.waitForInitialization();
@ -1094,7 +1094,7 @@ export class ActiveDoc extends EventEmitter {
]);
// Migrate the document if needed.
const values = marshal.loads(docInfoData!);
const values = marshal.loads(docInfoData);
const versionCol = values.schemaVersion;
const docSchemaVersion = (versionCol && versionCol.length === 1 ? versionCol[0] : 0);
if (docSchemaVersion < schemaVersion) {
@ -1126,7 +1126,7 @@ export class ActiveDoc extends EventEmitter {
const tableNames: string[] = await this._rawPyCall('load_meta_tables', tablesData, columnsData);
// Figure out which tables are on-demand.
const tablesParsed: BulkColValues = marshal.loads(tablesData!);
const tablesParsed: BulkColValues = marshal.loads(tablesData);
const onDemandMap = zipObject(tablesParsed.tableId as string[], tablesParsed.onDemand);
const onDemandNames = remove(tableNames, (t) => onDemandMap[t]);
@ -1183,7 +1183,7 @@ export class ActiveDoc extends EventEmitter {
const result: ApplyUAResult = await new Promise<ApplyUAResult>(
(resolve, reject) =>
this._sharing!.addUserAction({action, docSession, resolve, reject}));
this._sharing.addUserAction({action, docSession, resolve, reject}));
this.logDebug(docSession, "_applyUserActions returning %s", shortDesc(result));
if (result.isModification) {

@ -4,7 +4,7 @@
* of the client-side code.
*/
import * as express from 'express';
import fetch, {RequestInit, Response as FetchResponse} from 'node-fetch';
import fetch, {Response as FetchResponse, RequestInit} from 'node-fetch';
import {ApiError} from 'app/common/ApiError';
import {getSlugIfNeeded, parseSubdomainStrictly} from 'app/common/gristUrls';

@ -215,10 +215,9 @@ export class DocWorkerApi {
// Initiate a fork. Used internally to implement ActiveDoc.fork. Only usable via a Permit.
this._app.post('/api/docs/:docId/create-fork', canEdit, throttled(async (req, res) => {
const mreq = req as RequestWithLogin;
const docId = stringParam(req.params.docId);
const srcDocId = stringParam(req.body.srcDocId);
if (srcDocId !== mreq.specialPermit?.otherDocId) { throw new Error('access denied'); }
if (srcDocId !== req.specialPermit?.otherDocId) { throw new Error('access denied'); }
await this._docManager.storageManager.prepareFork(srcDocId, docId);
res.json({srcDocId, docId});
}));
@ -249,7 +248,7 @@ export class DocWorkerApi {
this._app.post('/api/docs/:docId/recover', canEdit, throttled(async (req, res) => {
const recoveryModeRaw = req.body.recoveryMode;
const recoveryMode = (typeof recoveryModeRaw === 'boolean') ? recoveryModeRaw : undefined;
if (!this._isOwner(req)) { throw new Error('Only owners can control recovery mode'); }
if (!await this._isOwner(req)) { throw new Error('Only owners can control recovery mode'); }
const activeDoc = await this._docManager.fetchDoc(docSessionFromRequest(req), getDocId(req), recoveryMode);
res.json({
recoveryMode: activeDoc.recoveryMode

@ -16,7 +16,8 @@ import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
import {assertAccess, Authorizer, DocAuthorizer, DummyAuthorizer,
isSingleUserMode} from 'app/server/lib/Authorizer';
import {Client} from 'app/server/lib/Client';
import {getDocSessionCachedDoc, makeExceptionalDocSession, makeOptDocSession, OptDocSession} from 'app/server/lib/DocSession';
import {getDocSessionCachedDoc, makeExceptionalDocSession, makeOptDocSession} from 'app/server/lib/DocSession';
import {OptDocSession} from 'app/server/lib/DocSession';
import * as docUtils from 'app/server/lib/docUtils';
import {GristServer} from 'app/server/lib/GristServer';
import {IDocStorageManager} from 'app/server/lib/IDocStorageManager';
@ -498,7 +499,7 @@ export class DocManager extends EventEmitter {
// TODO: We should be skeptical of the upload file to close a possible
// security vulnerability. See https://phab.getgrist.com/T457.
const docName = await this._createNewDoc(id);
const docPath = await this.storageManager.getPath(docName);
const docPath: string = this.storageManager.getPath(docName);
await docUtils.copyFile(uploadInfo.files[0].absPath, docPath);
await this.storageManager.addToStorage(docName);
return {title: basename, id: docName};

@ -65,7 +65,12 @@ export class DocPluginManager {
private _pluginInstances: PluginInstance[];
constructor(private _localPlugins: LocalPlugin[], private _appRoot: string, private _activeDoc: ActiveDoc, private _server: GristServer) {
constructor(
private _localPlugins: LocalPlugin[],
private _appRoot: string,
private _activeDoc: ActiveDoc,
private _server: GristServer
) {
this.gristDocAPI = new GristDocAPIImpl(_activeDoc);
this._pluginInstances = [];
this.ready = this._initialize();

@ -439,7 +439,9 @@ export class DocStorage implements ISQLiteDB {
* Note that SQLite may contain tables that aren't used for Grist data (e.g. attachments), for
* which such encoding/marshalling is not used, and e.g. binary data is stored to BLOBs directly.
*/
private static _encodeValue(marshaller: marshal.Marshaller, sqlType: string, val: any): Uint8Array|string|number|boolean {
private static _encodeValue(
marshaller: marshal.Marshaller, sqlType: string, val: any
): Uint8Array|string|number|boolean {
const marshalled = () => {
marshaller.marshal(val);
return marshaller.dump();
@ -1376,7 +1378,7 @@ export class DocStorage implements ISQLiteDB {
// columns, or adding or removing or changing default values on a column."
const row = await this.get("PRAGMA schema_version");
assert(row && row.schema_version, "Could not retrieve schema_version.");
const newSchemaVersion = row!.schema_version + 1;
const newSchemaVersion = row.schema_version + 1;
const tmpTableId = DocStorage._makeTmpTableId(tableId);
await this._getDB().runEach(
"PRAGMA writable_schema=ON",
@ -1462,7 +1464,7 @@ export class DocStorage implements ISQLiteDB {
* should be reasonably fast:
* https://sqlite.org/tempfiles.html#temp_databases
*/
public async _fetchQueryWithManyParameters(query: ExpandedQuery): Promise<Buffer> {
private async _fetchQueryWithManyParameters(query: ExpandedQuery): Promise<Buffer> {
const db = this._getDB();
return db.execTransaction(async () => {
const tableNames: string[] = [];
@ -1490,7 +1492,7 @@ export class DocStorage implements ISQLiteDB {
* Construct SQL for an ExpandedQuery. Expects that filters have been converted into
* a set of WHERE terms that should be ANDed.
*/
public _getSqlForQuery(query: ExpandedQuery, whereParts: string[]) {
private _getSqlForQuery(query: ExpandedQuery, whereParts: string[]) {
const whereClause = whereParts.length > 0 ? `WHERE ${whereParts.join(' AND ')}` : '';
const limitClause = (typeof query.limit === 'number') ? `LIMIT ${query.limit}` : '';
const joinClauses = query.joins ? query.joins.join(' ') : '';

@ -196,7 +196,7 @@ export class DocStorageManager implements IDocStorageManager {
* Electron version only. Shows the given doc in the file explorer.
*/
public async showItemInFolder(docName: string): Promise<void> {
this._shell.showItemInFolder(await this.getPath(docName));
this._shell.showItemInFolder(this.getPath(docName));
}
public async closeStorage() {

@ -133,7 +133,7 @@ export class KeyMappedExternalStorage implements ExternalStorage {
export class ChecksummedExternalStorage implements ExternalStorage {
private _closed: boolean = false;
constructor(readonly label: string, private _ext: ExternalStorage, private _options: {
constructor(public readonly label: string, private _ext: ExternalStorage, private _options: {
maxRetries: number, // how many time to retry inconsistent downloads
initialDelayMs: number, // how long to wait before retrying
localHash: PropStorage, // key/value store for hashes of downloaded content

@ -145,7 +145,7 @@ export class FlexServer implements GristServer {
private _sendAppPage: (req: express.Request, resp: express.Response, options: ISendAppPageOptions) => Promise<void>;
constructor(public port: number, public name: string = 'flexServer',
readonly options: FlexServerOptions = {}) {
public readonly options: FlexServerOptions = {}) {
this.app = express();
this.app.set('port', port);
this.appRoot = getAppRoot();

@ -724,7 +724,9 @@ export class GranularAccess implements GranularAccessForBundle {
const ruler = await this._getRuler(cursor);
const tableId = getTableId(action);
const ruleSets = ruler.ruleCollection.getAllColumnRuleSets(tableId);
const colIds = new Set(([] as string[]).concat(...ruleSets.map(ruleSet => ruleSet.colIds === '*' ? [] : ruleSet.colIds)));
const colIds = new Set(([] as string[]).concat(
...ruleSets.map(ruleSet => ruleSet.colIds === '*' ? [] : ruleSet.colIds)
));
const access = await ruler.getAccess(cursor.docSession);
// Check columns in a consistent order, for determinism (easier testing).
// TODO: could pool some work between columns by doing them together rather than one by one.
@ -1164,7 +1166,9 @@ export class GranularAccess implements GranularAccessForBundle {
const rowsBefore = cloneDeep(tableData?.getTableDataAction() || ['TableData', '', [], {}] as TableDataAction);
docData.receiveAction(docAction);
// If table is deleted, state afterwards doesn't matter.
const rowsAfter = docData.getTable(tableId) ? cloneDeep(tableData?.getTableDataAction() || ['TableData', '', [], {}] as TableDataAction) : rowsBefore;
const rowsAfter = docData.getTable(tableId) ?
cloneDeep(tableData?.getTableDataAction() || ['TableData', '', [], {}] as TableDataAction) :
rowsBefore;
const step: ActionStep = {action: docAction, rowsBefore, rowsAfter};
steps.push(step);
}
@ -1208,7 +1212,7 @@ export class GranularAccess implements GranularAccessForBundle {
if (applied) {
// Rules may have changed - back them off to a copy of their original state.
ruler = new Ruler(this);
ruler.update(metaDocData);
await ruler.update(metaDocData);
}
let replaceRuler = false;
for (const docAction of docActions) {
@ -1228,7 +1232,7 @@ export class GranularAccess implements GranularAccessForBundle {
replaceRuler = true;
} else if (replaceRuler) {
ruler = new Ruler(this);
ruler.update(metaDocData);
await ruler.update(metaDocData);
replaceRuler = false;
}
step.ruler = ruler;
@ -1625,7 +1629,8 @@ export class CensorshipInfo {
const tableId = tableRefToTableId.get(tableRef);
if (!tableId) { throw new Error('table not found'); }
const colId = rec.get('colId') as string;
if (this.censoredTables.has(tableRef) || (colId !== 'manualSort' && permInfo.getColumnAccess(tableId, colId).perms.read === 'deny')) {
if (this.censoredTables.has(tableRef) ||
(colId !== 'manualSort' && permInfo.getColumnAccess(tableId, colId).perms.read === 'deny')) {
censoredColumnCodes.add(columnCode(tableRef, colId));
}
}

@ -239,7 +239,7 @@ export class HostedStorageManager implements IDocStorageManager {
await this.prepareLocalDoc(docName, 'new');
if (this._inventory) {
await this._inventory.create(docName);
this._onInventoryChange(docName);
await this._onInventoryChange(docName);
}
this.markAsChanged(docName);
}

@ -10,18 +10,14 @@ export const ITestingHooks = t.iface([], {
"updateAuthToken": t.func("void", t.param("instId", "string"), t.param("authToken", "string")),
"getAuthToken": t.func(t.union("string", "null"), t.param("instId", "string")),
"useTestToken": t.func("void", t.param("instId", "string"), t.param("token", "string")),
"setLoginSessionProfile": t.func("void", t.param("gristSidCookie", "string"),
t.param("profile", t.union("UserProfile", "null")), t.param("org", "string", true)),
"setLoginSessionProfile": t.func("void", t.param("gristSidCookie", "string"), t.param("profile", t.union("UserProfile", "null")), t.param("org", "string", true)),
"setServerVersion": t.func("void", t.param("version", t.union("string", "null"))),
"disconnectClients": t.func("void"),
"commShutdown": t.func("void"),
"commRestart": t.func("void"),
"commSetClientPersistence": t.func("void", t.param("ttlMs", "number")),
"closeDocs": t.func("void"),
"setDocWorkerActivation": t.func("void", t.param("workerId", "string"),
t.param("active", t.union(t.lit('active'),
t.lit('inactive'),
t.lit('crash')))),
"setDocWorkerActivation": t.func("void", t.param("workerId", "string"), t.param("active", t.union(t.lit('active'), t.lit('inactive'), t.lit('crash')))),
"flushAuthorizerCache": t.func("void"),
"getDocClientCounts": t.func(t.array(t.tuple("string", "number"))),
"setActiveDocTimeout": t.func("number", t.param("seconds", "number")),
@ -29,6 +25,5 @@ export const ITestingHooks = t.iface([], {
const exportedTypeSuite: t.ITypeSuite = {
ITestingHooks,
UserProfile: t.name("object"),
};
export default exportedTypeSuite;

@ -1,8 +1,8 @@
import {UserProfile} from 'app/common/LoginSessionAPI';
export interface ITestingHooks {
getOwnPort(): number;
getPort(): number;
getOwnPort(): Promise<number>;
getPort(): Promise<number>;
updateAuthToken(instId: string, authToken: string): Promise<void>;
getAuthToken(instId: string): Promise<string|null>;
useTestToken(instId: string, token: string): Promise<void>;

@ -62,7 +62,7 @@ export class OnDemandActions {
// Check that the actionType can be applied without the sandbox and also that the action
// is on a data table.
const isOnDemandAction = ACTION_TYPES.has(a[0] as string);
const isDataTableAction = typeof a[1] === 'string' && !(a[1] as string).startsWith('_grist_');
const isDataTableAction = typeof a[1] === 'string' && !a[1].startsWith('_grist_');
if (a[0] === 'ApplyUndoActions') {
// Split actions inside the undo action array.
const [undoNormal, undoOnDemand] = this.splitByOnDemand(a[1] as UserAction[]);

@ -155,7 +155,7 @@ export class Sharing {
private async _rebaseLocalActions(): Promise<void> {
const rebaseQueue: Deque<UserActionBundle> = new Deque<UserActionBundle>();
try {
await this.createCheckpoint();
this.createCheckpoint();
const actions: LocalActionBundle[] = await this._actionHistory.fetchAllLocal();
assert(actions.length > 0);
await this.doApplyUserActionBundle(this._createUndo(actions), null);
@ -163,7 +163,7 @@ export class Sharing {
await this._actionHistory.clearLocalActions();
} catch (e) {
log.error("Can't undo local actions; sharing is off");
await this.rollbackToCheckpoint();
this.rollbackToCheckpoint();
// TODO this.disconnect();
// TODO errorState = true;
return;
@ -185,11 +185,11 @@ export class Sharing {
}
}
if (rebaseFailures.length > 0) {
await this.createBackupAtCheckpoint();
this.createBackupAtCheckpoint();
// TODO we should notify the user too.
log.error('Rebase failed to reapply some of your actions, backup of local at...');
}
await this.releaseCheckpoint();
this.releaseCheckpoint();
}
// ======================================================================
@ -374,7 +374,9 @@ export class Sharing {
const docActions = getEnvContent(sandboxActionBundle.stored).concat(
getEnvContent(sandboxActionBundle.calc));
const accessControl = this._activeDoc.getGranularAccessForBundle(docSession || makeExceptionalDocSession('share'), docActions, undo, userActions);
const accessControl = this._activeDoc.getGranularAccessForBundle(
docSession || makeExceptionalDocSession('share'), docActions, undo, userActions
);
try {
// TODO: see if any of the code paths that have no docSession are relevant outside
// of tests.

@ -6,13 +6,15 @@ import * as Comm from 'app/server/lib/Comm';
import {ILoginSession} from 'app/server/lib/ILoginSession';
import * as log from 'app/server/lib/log';
import {IMessage, Rpc} from 'grain-rpc';
import {createCheckers} from 'ts-interface-checker';
import * as t from 'ts-interface-checker';
import {FlexServer} from './FlexServer';
import {IInstanceManager} from './IInstanceManager';
import {ITestingHooks} from './ITestingHooks';
import ITestingHooksTI from './ITestingHooks-ti';
import {connect, fromCallback} from './serverUtils';
const tiCheckers = t.createCheckers(ITestingHooksTI, {UserProfile: t.name("object")});
export function startTestingHooks(socketPath: string, port: number, instanceManager: IInstanceManager,
comm: Comm, flexServer: FlexServer,
workerServers: FlexServer[]): Promise<net.Server> {
@ -27,7 +29,7 @@ export function startTestingHooks(socketPath: string, port: number, instanceMana
// Register the testing implementation.
rpc.registerImpl('testing',
new TestingHooks(port, instanceManager, comm, flexServer, workerServers),
createCheckers(ITestingHooksTI).ITestingHooks);
tiCheckers.ITestingHooks);
});
server.listen(socketPath);
});
@ -47,7 +49,7 @@ export interface TestingHooksClient extends ITestingHooks {
export async function connectTestingHooks(socketPath: string): Promise<TestingHooksClient> {
const socket = await connect(socketPath);
const rpc = connectToSocket(new Rpc({logger: {}}), socket);
return Object.assign(rpc.getStub<TestingHooks>('testing', createCheckers(ITestingHooksTI).ITestingHooks), {
return Object.assign(rpc.getStub<TestingHooks>('testing', tiCheckers.ITestingHooks), {
close: () => socket.end(),
});
}
@ -61,12 +63,12 @@ export class TestingHooks implements ITestingHooks {
private _workerServers: FlexServer[]
) {}
public getOwnPort(): number {
public async getOwnPort(): Promise<number> {
log.info("TestingHooks.getOwnPort called");
return this._server.getOwnPort();
}
public getPort(): number {
public async getPort(): Promise<number> {
log.info("TestingHooks.getPort called");
return this._port;
}
@ -100,7 +102,7 @@ export class TestingHooks implements ITestingHooks {
log.info("TestingHooks.setServerVersion called with", version);
this._comm.setServerVersion(version);
for (const server of this._workerServers) {
await server.comm.setServerVersion(version);
server.comm.setServerVersion(version);
}
}
@ -108,7 +110,7 @@ export class TestingHooks implements ITestingHooks {
log.info("TestingHooks.disconnectClients called");
this._comm.destroyAllClients();
for (const server of this._workerServers) {
await server.comm.destroyAllClients();
server.comm.destroyAllClients();
}
}
@ -134,7 +136,7 @@ export class TestingHooks implements ITestingHooks {
log.info("TestingHooks.setClientPersistence called with", ttlMs);
this._comm.testSetClientPersistence(ttlMs);
for (const server of this._workerServers) {
await server.comm.testSetClientPersistence(ttlMs);
server.comm.testSetClientPersistence(ttlMs);
}
}
@ -174,7 +176,7 @@ export class TestingHooks implements ITestingHooks {
log.info("TestingHooks.flushAuthorizerCache called");
this._server.dbManager.flushDocAuthCache();
for (const server of this._workerServers) {
await server.dbManager.flushDocAuthCache();
server.dbManager.flushDocAuthCache();
}
}

@ -316,7 +316,7 @@ export async function createTmpDir(options: tmp.Options): Promise<TmpDirResult>
try {
// Still call the original callback, so that `tmp` module doesn't keep remembering about
// this directory and doesn't try to delete it again on exit.
tmpCleanup();
await tmpCleanup();
} catch (err) {
// OK if it fails because the dir is already removed.
}

@ -21,7 +21,7 @@
},
"composite": true,
"plugins": [{
"name": "typescript-tslint-plugin"
"name": "typescript-eslint-language-service"
}],
"emitDecoratorMetadata": true,
"experimentalDecorators": true,

@ -4,7 +4,7 @@ export type ProductFlavor = string;
export interface CustomTheme {
bodyClassName?: string;
wideLogo?: boolean;
};
}
export function getTheme(flavor: string): CustomTheme {
return {

@ -15,7 +15,7 @@ import { decodeUrl } from 'app/common/gristUrls';
import { FullUser, UserProfile } from 'app/common/LoginSessionAPI';
import { resetOrg } from 'app/common/resetOrg';
import { TestState } from 'app/common/TestState';
import { DocStateComparison, Organization as APIOrganization, UserAPIImpl, Workspace } from 'app/common/UserAPI';
import { Organization as APIOrganization, DocStateComparison, UserAPIImpl, Workspace } from 'app/common/UserAPI';
import { Organization } from 'app/gen-server/entity/Organization';
import { Product } from 'app/gen-server/entity/Product';
import { create } from 'app/server/lib/create';
@ -163,7 +163,7 @@ export async function selectAll() {
* Returns a WebElementPromise for the .viewsection_content element for the section which contains
* the given RegExp content.
*/
export function getSection(sectionOrTitle: string|WebElement): WebElement {
export function getSection(sectionOrTitle: string|WebElement): WebElement|WebElementPromise {
if (typeof sectionOrTitle !== 'string') { return sectionOrTitle; }
return driver.find(`.test-viewsection-title[value="${sectionOrTitle}" i]`)
.findClosest('.viewsection_content');
@ -1187,7 +1187,7 @@ export class Session {
// Wipe the current site. The current user ends up being its only owner and manager.
public async resetSite() {
return resetOrg(await this.createHomeApi(), this.settings.org);
return resetOrg(this.createHomeApi(), this.settings.org);
}
// Return a session configured for the current session's site but a different user.

@ -55,7 +55,7 @@ export class TestServerMerged implements IMochaServer {
public async restart(reset: boolean = false) {
if (this.isExternalServer()) { return; }
if (this._starts > 0) {
await this.resume();
this.resume();
await this.stop();
}
this._starts++;

Loading…
Cancel
Save