(core) Speed up and upgrade build.

Summary:
- Upgrades to build-related packages:
  - Upgrade typescript, related libraries and typings.
  - Upgrade webpack, eslint; add tsc-watch, node-dev, eslint_d.

- Build organization changes:
  - Build webpack from original typescript, transpiling only; with errors still
    reported by a background tsc watching process.

- Typescript-related changes:
  - Reduce imports of AWS dependencies (very noticeable speedup)
  - Avoid auto-loading global @types
  - Client code is now built with isolatedModules flag (for safe transpilation)
  - Use allowJs to avoid copying JS files manually.

- Linting changes
  - Enhance Arcanist ESLintLinter to run before/after commands, and set up to use eslint_d
  - Update eslint config, and include .eslintignore to avoid linting generated files.
  - Include a bunch of eslint-prompted and eslint-generated fixes
  - Add no-unused-expression rule to eslint, and fix a few warnings about it

- Other items:
  - Refactor cssInput to avoid circular dependency
  - Remove a bit of unused code, libraries, dependencies

Test Plan: No behavior changes, all existing tests pass. There are 30 tests fewer reported because `test_gpath.py` was removed (it's been unused for years)

Reviewers: paulfitz

Reviewed By: paulfitz

Subscribers: paulfitz

Differential Revision: https://phab.getgrist.com/D3498
pull/214/head
Dmitry S 2 years ago
parent 64ff9ccd0a
commit dd2eadc86e

@ -81,7 +81,7 @@ export class Cursor extends Disposable {
this.viewData = baseView.viewData;
this._sectionId = this.autoDispose(ko.computed(() => baseView.viewSection.id()));
this._rowId = ko.observable(optCursorPos.rowId || 0);
this._rowId = ko.observable<RowId|null>(optCursorPos.rowId || 0);
this.rowIndex = this.autoDispose(ko.computed({
read: () => {
if (!this._isLive()) { return this.rowIndex.peek(); }

@ -93,7 +93,7 @@ export class TypeTransform extends ColumnTransform {
const colInfo = await TypeConversion.prepTransformColInfo(docModel, this.origColumn, this.origDisplayCol, toType);
// NOTE: We could add rules with AddColumn action, but there are some optimizations that converts array values.
const rules = colInfo.rules;
delete colInfo.rules;
delete (colInfo as any).rules;
const newColInfos = await this._tableData.sendTableActions([
['AddColumn', 'gristHelper_Converted', {...colInfo, isFormula: false, formula: ''}],
['AddColumn', 'gristHelper_Transform', colInfo],

@ -1,6 +1,7 @@
import { GristDoc } from 'app/client/components/GristDoc';
import { ViewFieldRec, ViewSectionRec } from 'app/client/models/DocModel';
import { cssField, cssInput, cssLabel} from 'app/client/ui/MakeCopyMenu';
import { cssInput } from 'app/client/ui/cssInput';
import { cssField, cssLabel } from 'app/client/ui/MakeCopyMenu';
import { IPageWidget, toPageWidget } from 'app/client/ui/PageWidgetPicker';
import { confirmModal } from 'app/client/ui2018/modals';
import { BulkColValues, getColValues, RowRecord, UserAction } from 'app/common/DocActions';

@ -12,7 +12,6 @@ declare module "app/client/lib/browserGlobals";
declare module "app/client/lib/dom";
declare module "app/client/lib/koDom";
declare module "app/client/lib/koForm";
declare module "app/client/lib/koSession";
declare module "app/client/widgets/UserType";
declare module "app/client/widgets/UserTypeImpl";
@ -319,3 +318,9 @@ declare module "app/client/lib/koUtil" {
// with polyfills for old browsers.
declare module "bowser/bundled";
declare module "randomcolor";
interface Location {
// We use reload(true) in places, which has an effect in Firefox, but may be more of a
// historical accident than an intentional choice.
reload(forceGet?: boolean): void;
}

@ -8,7 +8,9 @@
exports.loadBillingPage = () => import('app/client/ui/BillingPage' /* webpackChunkName: "BillingModule" */);
exports.loadGristDoc = () => import('app/client/components/GristDoc' /* webpackChunkName: "GristDoc" */);
exports.loadMomentTimezone = () => import('moment-timezone');
// When importing this way, the module is under the "default" member, not sure why (maybe
// esbuild-loader's doing).
exports.loadMomentTimezone = () => import('moment-timezone').then(m => m.default);
exports.loadPlotly = () => import('plotly.js-basic-dist' /* webpackChunkName: "plotly" */);
exports.loadSearch = () => import('app/client/ui2018/search' /* webpackChunkName: "search" */);
exports.loadUserManager = () => import('app/client/ui/UserManager' /* webpackChunkName: "usermanager" */);

@ -1,90 +0,0 @@
/**
* koSession offers observables whose values are tied to the browser session or history:
*
* sessionValue(key) - an observable preserved across history entries and reloads.
*
* Note: we could also support "browserValue", shared across all tabs and across browser restarts
* (same as sessionValue but using window.localStorage), but it seems more appropriate to store
* such values on the server.
*/
/* global window, $ */
var _ = require('underscore');
var ko = require('knockout');
/**
* Maps a string key to an observable. The space of keys is shared for all kinds of observables,
* and they differ only in where they store their state. Each observable gets several extra
* properties:
* @property {String} ksKey The key used for storage. It should be unique across koSession values.
* @property {Object} ksDefault The default value if the storage doesn't have one.
* @property {Function} ksFetch The method to fetch the value from storage.
* @property {Function} ksSave The method to save the value to storage.
*/
var _sessionValues = {};
function createObservable(key, defaultValue, methods) {
var obs = _sessionValues[key];
if (!obs) {
_sessionValues[key] = obs = ko.observable();
obs.ksKey = key;
obs.ksDefaultValue = defaultValue;
obs.ksFetch = methods.fetch;
obs.ksSave = methods.save;
obs.dispose = methods.dispose;
// We initialize the observable before setting rateLimit, to ensure that the initialization
// doesn't end up triggering subscribers that are about to be added (which seems to be a bit
// of a problem with rateLimit extender, and possibly deferred). This workaround relies on the
// fact that the extender modifies its target without creating a new one.
obs(obs.ksFetch());
obs.extend({deferred: true});
obs.subscribe(function(newValue) {
if (newValue !== this.ksFetch()) {
console.log("koSession: %s changed %s -> %s", this.ksKey, this.ksFetch(), newValue);
this.ksSave(newValue);
}
}, obs);
}
return obs;
}
/**
* Returns an observable whose value sticks across reloads and navigation, but is different for
* different browser tabs. E.g. it may be used to reflect whether a side pane is open.
* The `key` isn't visible to the user, so pick any unique string name.
*/
function sessionValue(key, optDefault) {
return createObservable(key, optDefault, sessionValueMethods);
}
exports.sessionValue = sessionValue;
var sessionValueMethods = {
'fetch': function() {
var value = window.sessionStorage.getItem(this.ksKey);
if (!value) {
return this.ksDefaultValue;
}
try {
return JSON.parse(value);
} catch (e) {
return this.ksDefaultValue;
}
},
'save': function(value) {
window.sessionStorage.setItem(this.ksKey, JSON.stringify(value));
},
'dispose': function(value) {
window.sessionStorage.removeItem(this.ksKey);
}
};
function onApplyState() {
_.each(_sessionValues, function(obs, key) {
obs(obs.ksFetch());
});
}
$(window).on('applyState', onApplyState);

@ -21,13 +21,12 @@ export class DataRowModel extends BaseRowModel {
public _validationFailures: ko.PureComputed<Array<IRowModel<'_grist_Validations'>>>;
public _isAddRow: ko.Observable<boolean>;
private _allValidationsList: ko.Computed<KoArray<ValidationRec>>;
private _isRealChange: ko.Observable<boolean>;
public constructor(dataTableModel: DataTableModel, colNames: string[]) {
super(dataTableModel, colNames);
this._allValidationsList = dataTableModel.tableMetaRow.validations;
const allValidationsList: ko.Computed<KoArray<ValidationRec>> = dataTableModel.tableMetaRow.validations;
this._isAddRow = ko.observable(false);
@ -36,10 +35,10 @@ export class DataRowModel extends BaseRowModel {
// changes, those should only be enabled when _isRealChange is true.
this._isRealChange = ko.observable(true);
this._validationFailures = this.autoDispose(ko.pureComputed(function() {
return this._allValidationsList().all().filter(
this._validationFailures = this.autoDispose(ko.pureComputed(() => {
return allValidationsList().all().filter(
validation => !this.cells[this.getValidationNameFromId(validation.id())]());
}, this));
}));
}
/**

@ -41,16 +41,8 @@ import {decodeObject} from 'app/plugin/objtypes';
// Re-export all the entity types available. The recommended usage is like this:
// import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel';
export {ColumnRec} from 'app/client/models/entities/ColumnRec';
export {DocInfoRec} from 'app/client/models/entities/DocInfoRec';
export {FilterRec} from 'app/client/models/entities/FilterRec';
export {PageRec} from 'app/client/models/entities/PageRec';
export {TabBarRec} from 'app/client/models/entities/TabBarRec';
export {TableRec} from 'app/client/models/entities/TableRec';
export {ValidationRec} from 'app/client/models/entities/ValidationRec';
export {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
export {ViewRec} from 'app/client/models/entities/ViewRec';
export {ViewSectionRec} from 'app/client/models/entities/ViewSectionRec';
export type {ColumnRec, DocInfoRec, FilterRec, PageRec, TabBarRec, TableRec, ValidationRec,
ViewFieldRec, ViewRec, ViewSectionRec};
/**

@ -6,7 +6,7 @@ import {buildColFilter, ColumnFilterFunc} from 'app/common/ColumnFilterFunc';
import {buildRowFilter, RowFilterFunc, RowValueFunc } from 'app/common/RowFilterFunc';
import {Computed, Disposable, MutableObsArray, obsArray, Observable, UseCB} from 'grainjs';
export {ColumnFilterFunc} from 'app/common/ColumnFilterFunc';
export type {ColumnFilterFunc};
interface OpenColumnFilter {
colRef: number;

@ -306,7 +306,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
// in which case the UI prevents various things like hiding columns or changing the widget type.
this.isRaw = this.autoDispose(ko.pureComputed(() => this.table().rawViewSectionRef() === this.getRowId()));
this.borderWidthPx = ko.pureComputed(function() { return this.borderWidth() + 'px'; }, this);
this.borderWidthPx = ko.pureComputed(() => this.borderWidth() + 'px');
this.layoutSpecObj = modelUtil.jsonObservable(this.layoutSpec);
@ -487,7 +487,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
this.linkSrcCol = refRecord(docModel.columns, this.activeLinkSrcColRef);
this.linkTargetCol = refRecord(docModel.columns, this.activeLinkTargetColRef);
this.activeRowId = ko.observable(null);
this.activeRowId = ko.observable<RowId|null>(null);
this._linkingState = Holder.create(this);
this.linkingState = this.autoDispose(ko.pureComputed(() => {
@ -506,7 +506,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
}));
// If the view instance for this section is instantiated, it will be accessible here.
this.viewInstance = ko.observable(null);
this.viewInstance = ko.observable<BaseView|null>(null);
// Describes the most recent cursor position in the section.
this.lastCursorPos = {
@ -542,8 +542,8 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
);
this.hasCustomOptions = ko.observable(false);
this.desiredAccessLevel = ko.observable(null);
this.columnsToMap = ko.observable(null);
this.desiredAccessLevel = ko.observable<AccessLevel|null>(null);
this.columnsToMap = ko.observable<ColumnsToMap|null>(null);
// Calculate mapped columns for Custom Widget.
this.mappedColumns = ko.pureComputed(() => {
// First check if widget has requested a custom column mapping and

@ -236,7 +236,7 @@ export class BillingPage extends Disposable {
moneyPlan?.amount ? [
makeSummaryFeature([`Your team site has `, `${sub.userCount}`,
` member${sub.userCount !== 1 ? 's' : ''}`]),
tier ? this._makeAppSumoFeature(discountName!) : null,
tier ? this._makeAppSumoFeature(discountName) : null,
// Currently the subtotal is misleading and scary when tiers are in effect.
// In this case, for now, just report what will be invoiced.
!tier ? makeSummaryFeature([`Your ${moneyPlan.interval}ly subtotal is `,

@ -6,6 +6,7 @@
import {AppModel, reportError} from 'app/client/models/AppModel';
import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
import {getWorkspaceInfo, ownerName, workspaceName} from 'app/client/models/WorkspaceInfo';
import {cssInput} from 'app/client/ui/cssInput';
import {bigBasicButton, bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
import {colors, testId, vars} from 'app/client/ui2018/cssVars';
@ -275,16 +276,6 @@ class SaveCopyModal extends Disposable {
}
}
export const cssInput = styled('input', `
height: 30px;
width: 100%;
font-size: ${vars.mediumFontSize};
border-radius: 3px;
padding: 5px;
border: 1px solid ${colors.darkGrey};
outline: none;
`);
export const cssField = styled('div', `
margin: 16px 0;
display: flex;

@ -0,0 +1,12 @@
import {colors, vars} from 'app/client/ui2018/cssVars';
import {styled} from 'grainjs';
export const cssInput = styled('input', `
height: 30px;
width: 100%;
font-size: ${vars.mediumFontSize};
border-radius: 3px;
padding: 5px;
border: 1px solid ${colors.darkGrey};
outline: none;
`);

@ -75,7 +75,7 @@ export function prepareForTransition(elem: HTMLElement, prepare: () => void) {
// Recompute styles while transitions are off. See https://stackoverflow.com/a/16575811/328565
// for explanation and https://stackoverflow.com/a/31862081/328565 for the recommendation used
// here to trigger a style computation without a reflow.
window.getComputedStyle(elem).opacity; // tslint:disable-line:no-unused-expression
window.getComputedStyle(elem).opacity; // eslint-disable-line no-unused-expressions
// Restore transitions.
elem.style.transitionProperty = prior;

@ -1,6 +1,6 @@
import {FocusLayer} from 'app/client/lib/FocusLayer';
import {reportError} from 'app/client/models/errors';
import {cssInput} from 'app/client/ui/MakeCopyMenu';
import {cssInput} from 'app/client/ui/cssInput';
import {prepareForTransition, TransitionWatcher} from 'app/client/ui/transitions';
import {bigBasicButton, bigPrimaryButton, cssButton} from 'app/client/ui2018/buttons';
import {colors, mediaSmall, testId, vars} from 'app/client/ui2018/cssVars';

@ -29,7 +29,7 @@ export class ChoiceTextBox extends NTextBox {
private _choiceValues: Computed<string[]>;
private _choiceValuesSet: Computed<Set<string>>;
private _choiceOptions: KoSaveableObservable<ChoiceOptions | null | undefined>;
private _choiceOptionsByName: Computed<ChoiceOptionsByName>
private _choiceOptionsByName: Computed<ChoiceOptionsByName>;
constructor(field: ViewFieldRec) {
super(field);

@ -17,6 +17,9 @@ export interface IMargins {
right: number;
}
export type IRect = ISize & IMargins;
// edgeMargin is how many pixels to leave before the edge of the browser window by default.
// This is added to margins that may be passed into the constructor.
const edgeMargin = 12;
@ -37,8 +40,8 @@ export class EditorPlacement extends Disposable {
public readonly onReposition = this.autoDispose(new Emitter());
private _editorRoot: HTMLElement;
private _maxRect: ClientRect|DOMRect;
private _cellRect: ClientRect|DOMRect;
private _maxRect: IRect;
private _cellRect: IRect;
private _margins: IMargins;
// - editorDom is the DOM to attach. It gets destroyed when EditorPlacement is disposed.
@ -141,7 +144,7 @@ export class EditorPlacement extends Disposable {
// Get the bounding rect of elem excluding borders. This allows the editor to match cellElem more
// closely which is more visible in case of DetailView.
function rectWithoutBorders(elem: Element): ClientRect {
function rectWithoutBorders(elem: Element): IRect {
const rect = elem.getBoundingClientRect();
const style = getComputedStyle(elem, null);
const bTop = parseFloat(style.getPropertyValue('border-top-width'));

@ -55,6 +55,7 @@ function getTypeDefinition(type: string | false) {
}
type ComputedStyle = {style?: Style; error?: true} | null | undefined;
/**
* Builds a font option computed property.
*/
@ -62,12 +63,13 @@ function buildFontOptions(
builder: FieldBuilder,
computedRule: ko.Computed<ComputedStyle>,
optionName: keyof Style) {
return koUtil.withKoUtils(ko.computed(function() {
return koUtil.withKoUtils(ko.computed(() => {
if (builder.isDisposed()) { return false; }
const style = computedRule()?.style;
const styleFlag = style?.[optionName] || this.field[optionName]();
const styleFlag = style?.[optionName] || builder.field[optionName]();
return styleFlag;
}, builder)).onlyNotifyUnequal();
})).onlyNotifyUnequal();
}
/**
@ -131,9 +133,9 @@ export class FieldBuilder extends Disposable {
});
// Observable which evaluates to a *function* that decides if a value is valid.
this._isRightType = ko.pureComputed(function() {
this._isRightType = ko.pureComputed<(value: CellValue, options?: any) => boolean>(() => {
return gristTypes.isRightType(this._readOnlyPureType()) || _.constant(false);
}, this);
});
// Returns a boolean indicating whether the column is type Reference or ReferenceList.
this._isRef = this.autoDispose(ko.computed(() => {
@ -154,7 +156,7 @@ export class FieldBuilder extends Disposable {
}
}));
this.widget = ko.pureComputed({
this.widget = ko.pureComputed<object>({
owner: this,
read() { return this.options().widget; },
write(widget) {
@ -196,10 +198,10 @@ export class FieldBuilder extends Disposable {
this._rowMap = new Map();
// Returns the constructor for the widget, and only notifies subscribers on changes.
this._widgetCons = this.autoDispose(koUtil.withKoUtils(ko.computed(function() {
this._widgetCons = this.autoDispose(koUtil.withKoUtils(ko.computed(() => {
return UserTypeImpl.getWidgetConstructor(this.options().widget,
this._readOnlyPureType());
}, this)).onlyNotifyUnequal());
})).onlyNotifyUnequal());
// Computed builder for the widget.
this.widgetImpl = this.autoDispose(koUtil.computedBuilder(() => {
@ -467,28 +469,28 @@ export class FieldBuilder extends Disposable {
return { style : new CombinedStyle(styles, flags) };
}, this).extend({ deferred: true })).previousOnUndefined();
const widgetObs = koUtil.withKoUtils(ko.computed(function() {
const widgetObs = koUtil.withKoUtils(ko.computed(() => {
// TODO: Accessing row values like this doesn't always work (row and field might not be updated
// simultaneously).
if (this.isDisposed()) { return null; } // Work around JS errors during field removal.
const value = row.cells[this.field.colId()];
const cell = value && value();
if (value && this._isRightType()(cell, this.options) || row._isAddRow.peek()) {
if ((value) && this._isRightType()(cell, this.options) || row._isAddRow.peek()) {
return this.widgetImpl();
} else if (gristTypes.isVersions(cell)) {
return this.diffImpl;
} else {
return null;
}
}, this).extend({ deferred: true })).onlyNotifyUnequal();
}).extend({ deferred: true })).onlyNotifyUnequal();
const textColor = koUtil.withKoUtils(ko.computed(function() {
const textColor = koUtil.withKoUtils(ko.computed(() => {
if (this.isDisposed()) { return null; }
const fromRules = computedRule()?.style?.textColor;
return fromRules || this.field.textColor() || '';
}, this)).onlyNotifyUnequal();
})).onlyNotifyUnequal();
const fillColor = koUtil.withKoUtils(ko.computed(function() {
const fillColor = koUtil.withKoUtils(ko.computed(() => {
if (this.isDisposed()) { return null; }
const fromRules = computedRule()?.style?.fillColor;
let fill = fromRules || this.field.fillColor();
@ -496,7 +498,7 @@ export class FieldBuilder extends Disposable {
// If there is no color we are using fully transparent white color (for tests mainly).
fill = fill ? fill.toUpperCase() : fill;
return (fill === '#FFFFFF' ? '' : fill) || '#FFFFFF00';
}, this)).onlyNotifyUnequal();
})).onlyNotifyUnequal();
const fontBold = buildFontOptions(this, computedRule, 'fontBold');
const fontItalic = buildFontOptions(this, computedRule, 'fontItalic');

@ -135,6 +135,7 @@ export class NTextEditor extends NewBaseEditor {
// but we got same enough spaces, we will force browser to check the available space once more time.
if (enoughSpace(rect, size) && hasScroll(textInput)) {
textInput.style.overflow = "hidden";
// eslint-disable-next-line no-unused-expressions
textInput.clientHeight; // just access metrics is enough to repaint
textInput.style.overflow = "auto";
}

@ -14,6 +14,7 @@ import {
dom,
DomContents,
fromKo,
IDisposableOwnerT,
Observable,
} from 'grainjs';
@ -30,8 +31,13 @@ export abstract class NewAbstractWidget extends Disposable {
/**
* Override the create() method to match the parameters of create() expected by FieldBuilder.
*/
public static create(field: ViewFieldRec) {
return Disposable.create.call(this as any, null, field);
// We copy Disposable.create() signature (the second one) to pacify typescript, but code must
// use the first signature, which is compatible with old-style constructors.
public static create<T extends new (...args: any[]) => any>(field: ViewFieldRec): InstanceType<T>;
public static create<T extends new (...args: any[]) => any>(
this: T, owner: IDisposableOwnerT<InstanceType<T>>|null, ...args: ConstructorParameters<T>): InstanceType<T>;
public static create(...args: any[]) {
return Disposable.create.call(this as any, null, ...args);
}
protected options: SaveableObjObservable<any>;

@ -4,7 +4,7 @@
// Some definitions have moved to be part of plugin API.
import { BulkColValues, CellValue, RowRecord } from 'app/plugin/GristData';
export { BulkColValues, CellValue, RowRecord } from 'app/plugin/GristData';
export type { BulkColValues, CellValue, RowRecord };
// Part of a special CellValue used for comparisons, embedding several versions of a CellValue.
export interface AllCellVersions {
@ -173,7 +173,7 @@ export function getNumRows(action: DocAction): number {
export function toTableDataAction(tableId: string, colValues: TableColValues): TableDataAction {
const colData = {...colValues}; // Make a copy to avoid changing passed-in arguments.
const rowIds: number[] = colData.id;
delete colData.id;
delete (colData as BulkColValues).id;
return ['TableData', tableId, rowIds, colData];
}

@ -67,7 +67,7 @@ export type AclMatchFunc = (input: AclMatchInput) => boolean;
* Representation of a parsed ACL formula.
*/
type PrimitiveCellValue = number|string|boolean|null;
export type ParsedAclFormula = [string, ...Array<ParsedAclFormula|PrimitiveCellValue>];
export type ParsedAclFormula = [string, ...(ParsedAclFormula|PrimitiveCellValue)[]];
/**
* Observations about a formula.

@ -13,9 +13,6 @@
*
*/
// important to explicitly import this, or webpack --watch gets confused.
import {clearTimeout, setTimeout} from "timers";
export class InactivityTimer {
private _timeout?: NodeJS.Timer | null;

@ -19,7 +19,7 @@ try {
const display = (code: string) => {
try {
const locale = new Intl.Locale(code);
const regionName = regionDisplay.of(locale.region);
const regionName = regionDisplay.of(locale.region!);
const languageName = languageDisplay.of(locale.language);
return `${regionName} (${languageName})`;
} catch (ex) {
@ -58,7 +58,7 @@ export function getCurrency(code: string) {
try {
const currencyDisplay = new Intl.DisplayNames('en', {type: 'currency'});
currencies = [...new Set(currenciesCodes)].map(code => {
return {name: currencyDisplay.of(code), code};
return {name: currencyDisplay.of(code)!, code};
});
} catch {
// Fall back to using the currency code as the display name.

@ -255,10 +255,6 @@ export class HomeDBManager extends EventEmitter {
// In restricted mode, documents should be read-only.
private _restrictedMode: boolean = false;
public emit(event: NotifierEvent, ...args: any[]): boolean {
return super.emit(event, ...args);
}
/**
* Five aclRules, each with one group (with the names 'owners', 'editors', 'viewers',
* 'guests', and 'members') are created by default on every new entity (Organization,
@ -298,6 +294,10 @@ export class HomeDBManager extends EventEmitter {
orgOnly: true
}];
public emit(event: NotifierEvent, ...args: any[]): boolean {
return super.emit(event, ...args);
}
// All groups.
public get defaultGroups(): GroupDescriptor[] {
return this._defaultGroups;

@ -1,5 +1,5 @@
// Letter codes for CellValue types encoded as [code, args...] tuples.
export const enum GristObjCode {
export enum GristObjCode {
List = 'L',
LookUp = 'l',
Dict = 'O',

@ -1,4 +1,4 @@
import {createCheckers, ICheckerSuite} from 'ts-interface-checker';
import {BasicType, createCheckers, ICheckerSuite} from 'ts-interface-checker';
import CustomSectionAPITI from './CustomSectionAPI-ti';
import FileParserAPITI from './FileParserAPI-ti';
import GristAPITI from './GristAPI-ti';
@ -18,7 +18,14 @@ export {
const allTypes = [
CustomSectionAPITI, FileParserAPITI, GristAPITI, GristTableTI, ImportSourceAPITI,
InternalImportSourceAPITI, RenderOptionsTI, StorageAPITI, WidgetAPITI];
InternalImportSourceAPITI, RenderOptionsTI, StorageAPITI, WidgetAPITI,
];
// Ensure Buffer can be handled if mentioned in the interface descriptions, even if not supported
// in the current environment (i.e. browser).
if (typeof Buffer === 'undefined') {
allTypes.push({Buffer: new BasicType((v) => false, "Buffer is not supported")});
}
function checkDuplicates(types: Array<{[key: string]: object}>) {
const seen = new Set<string>();

@ -402,7 +402,7 @@ export interface ReadyPayload extends Omit<InteractionOptionsRequest, "hasCustom
/**
* Handler that will be called by Grist to open additional configuration panel inside the Custom Widget.
*/
onEditOptions: () => unknown;
onEditOptions?: () => unknown;
}
/**
* Declare that a component is prepared to receive messages from the outside world.

@ -18,33 +18,6 @@ declare module "bluebird" {
class Disposer<T> {}
}
// TODO This is a module by Grist Labs; we should add index.d.ts to it.
declare module "@gristlabs/basket-api" {
interface Item { [colId: string]: any; }
interface ColValues { [colId: string]: any[]; }
interface AuthToken { [authProvider: string]: string; }
class Basket {
public static addBasket(login: AuthToken): Promise<string>;
public static getBaskets(login: AuthToken): Promise<string[]>;
public basketId: Readonly<string>;
public apiKey: Readonly<string|undefined>;
constructor(basketId: string, apiKey?: string);
public addTable(optTableId: string): Promise<string>;
public getTable(tableId: string): Promise<Item[]>;
public renameTable(oldTableId: string, newTableId: string): Promise<void>;
public replaceTableData(tableId: string, columnValues: ColValues): Promise<void>;
public deleteTable(tableId: string): Promise<void>;
public getTables(): Promise<string[]>;
public uploadAttachment(attachmentId: string, attachment: Buffer): Promise<void>;
public delete(login: AuthToken): Promise<void>;
}
namespace Basket {}
export = Basket;
}
// Used in one place, and the typings are almost entirely unhelpful.
declare module "multiparty";

@ -49,7 +49,7 @@ import {
adaptServerUrl, addOrgToPath, addPermit, getOrgUrl, getOriginUrl, getScope, optStringParam,
RequestWithGristInfo, stringParam, TEST_HTTPS_OFFSET, trustOrigin} from 'app/server/lib/requestUtils';
import {ISendAppPageOptions, makeGristConfig, makeMessagePage, makeSendAppPage} from 'app/server/lib/sendAppPage';
import {getDatabaseUrl} from 'app/server/lib/serverUtils';
import {getDatabaseUrl, listenPromise} from 'app/server/lib/serverUtils';
import {Sessions} from 'app/server/lib/Sessions';
import * as shutdown from 'app/server/lib/shutdown';
import {TagChecker} from 'app/server/lib/TagChecker';
@ -1666,14 +1666,11 @@ export class FlexServer implements GristServer {
private async _startServers(server: http.Server, httpsServer: https.Server|undefined,
name: string, port: number, verbose: boolean) {
await new Promise((resolve, reject) => server.listen(port, this.host, resolve).on('error', reject));
await listenPromise(server.listen(port, this.host));
if (verbose) { log.info(`${name} available at ${this.host}:${port}`); }
if (TEST_HTTPS_OFFSET && httpsServer) {
const httpsPort = port + TEST_HTTPS_OFFSET;
await new Promise((resolve, reject) => {
httpsServer.listen(httpsPort, this.host, resolve)
.on('error', reject);
});
await listenPromise(httpsServer.listen(httpsPort, this.host));
if (verbose) { log.info(`${name} available at https://${this.host}:${httpsPort}`); }
}
}

@ -1,5 +1,6 @@
export interface INotifier {
deleteUser(userId: number): Promise<void>;
// for test purposes, check if any notifications are in progress
readonly testPending: boolean;
deleteUser(userId: number): Promise<void>;
}

@ -125,16 +125,16 @@ export class NSandbox implements ISandbox {
if (options.minimalPipeMode) {
log.rawDebug("3-pipe Sandbox started", this._logMeta);
this._streamToSandbox = this.childProc.stdin;
this._streamFromSandbox = this.childProc.stdout;
this._streamToSandbox = this.childProc.stdin!;
this._streamFromSandbox = this.childProc.stdout!;
} else {
log.rawDebug("5-pipe Sandbox started", this._logMeta);
this._streamToSandbox = (this.childProc.stdio as Stream[])[3] as Writable;
this._streamFromSandbox = (this.childProc.stdio as Stream[])[4];
this.childProc.stdout.on('data', sandboxUtil.makeLinePrefixer('Sandbox stdout: ', this._logMeta));
this.childProc.stdout!.on('data', sandboxUtil.makeLinePrefixer('Sandbox stdout: ', this._logMeta));
}
const sandboxStderrLogger = sandboxUtil.makeLinePrefixer('Sandbox stderr: ', this._logMeta);
this.childProc.stderr.on('data', data => {
this.childProc.stderr!.on('data', data => {
this._lastStderr = data;
sandboxStderrLogger(data);
});

@ -16,7 +16,7 @@ import {IMsgCustom, IMsgRpcCall} from 'grain-rpc';
*/
export class SafePythonComponent extends BaseComponent {
private _sandbox: ISandbox;
private _sandbox?: ISandbox;
private _logMeta: log.ILogMeta;
// safe python component does not need pluginInstance.rpc because it is not possible to forward

@ -118,7 +118,7 @@ export class DocTriggers {
public shutdown() {
this._shuttingDown = true;
if (!this._sending) {
this._redisClientField?.quitAsync();
void(this._redisClientField?.quitAsync());
}
}

@ -121,8 +121,8 @@ export class UnsafeNodeComponent extends BaseComponent {
.catch(err => log.warn("unsafeNode[%s] failed with %s", child.pid, err))
.then(() => { this._child = undefined; });
child.stdout.on('data', makeLinePrefixer('PLUGIN stdout: '));
child.stderr.on('data', makeLinePrefixer('PLUGIN stderr: '));
child.stdout!.on('data', makeLinePrefixer('PLUGIN stdout: '));
child.stderr!.on('data', makeLinePrefixer('PLUGIN stderr: '));
warnIfNotReady(this._rpc, 3000, "Plugin isn't ready; be sure to call grist.ready() from plugin");
child.on('message', this._rpc.receiveMessage.bind(this._rpc));

@ -63,6 +63,13 @@ export function connect(arg: any, ...moreArgs: any[]): Promise<net.Socket> {
});
}
/**
* Promisified version of net.Server.listen().
*/
export function listenPromise<T extends net.Server>(server: T): Promise<void> {
return new Promise<void>((resolve, reject) => server.once('listening', resolve).once('error', reject));
}
/**
* Returns whether the path `inner` is contained within the directory `outer`.
*/

@ -1,10 +1,12 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"target": "es2016",
"target": "es2017",
"module": "commonjs",
"strict": true,
"strictPropertyInitialization": false,
"useUnknownInCatchVariables": false,
"skipLibCheck": true,
"sourceMap": true,
"noImplicitAny": true,
"noUnusedLocals": true,
@ -22,6 +24,7 @@
],
},
"composite": true,
"types" : [],
"plugins": [{
"name": "typescript-eslint-language-service"
}],

@ -3,9 +3,26 @@ const path = require('path');
module.exports = {
target: 'web',
mode: 'production',
entry: "./_build/app/client/browserCheck.js",
entry: "./app/client/browserCheck",
output: {
path: path.resolve("./static"),
filename: "browser-check.js"
},
resolve: {
extensions: ['.ts', '.js'],
},
module: {
rules: [
{
test: /\.(js|ts)?$/,
loader: 'esbuild-loader',
options: {
loader: 'ts',
target: 'es2017',
sourcemap: true,
},
exclude: /node_modules/
},
]
}
};

@ -1,13 +1,13 @@
const StatsPlugin = require('stats-webpack-plugin');
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
const { ProvidePlugin } = require('webpack');
const path = require('path');
module.exports = {
target: 'web',
entry: {
main: "app/client/app.js",
errorPages: "app/client/errorMain.js",
account: "app/client/accountMain.js",
main: "app/client/app",
errorPages: "app/client/errorMain",
account: "app/client/accountMain",
},
output: {
filename: "[name].bundle.js",
@ -32,15 +32,29 @@ module.exports = {
// typescript ("cheap-module-eval-source-map" is faster, but breakpoints are largely broken).
devtool: "source-map",
resolve: {
extensions: ['.ts', '.js'],
modules: [
path.resolve('./_build'),
path.resolve('./_build/ext'),
path.resolve('./_build/stubs'),
path.resolve('.'),
path.resolve('./ext'),
path.resolve('./stubs'),
path.resolve('./node_modules')
],
fallback: {
'path': require.resolve("path-browserify"),
},
},
module: {
rules: [
{
test: /\.(js|ts)?$/,
loader: 'esbuild-loader',
options: {
loader: 'ts',
target: 'es2017',
sourcemap: true,
},
exclude: /node_modules/
},
{ test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre"
@ -48,10 +62,11 @@ module.exports = {
]
},
plugins: [
new StatsPlugin(
'../.build_stats_js_bundle', // relative to output folder
{source: false}, // Omit sources, which unnecessarily make the stats file huge.
),
// Some modules assume presence of Buffer and process.
new ProvidePlugin({
process: 'process/browser',
Buffer: ['buffer', 'Buffer']
}),
// To strip all locales except “en”
new MomentLocalesPlugin()
],

@ -45,9 +45,8 @@
"@types/mime-types": "2.1.0",
"@types/mocha": "5.2.5",
"@types/moment-timezone": "0.5.9",
"@types/node": "^10",
"@types/node": "^14",
"@types/node-fetch": "2.1.2",
"@types/numeral": "0.0.25",
"@types/pidusage": "2.0.1",
"@types/plotly.js": "1.44.15",
"@types/qrcode": "1.4.2",
@ -64,6 +63,7 @@
"catw": "1.0.1",
"chai": "4.2.0",
"chai-as-promised": "7.1.1",
"esbuild-loader": "2.19.0",
"mocha": "5.2.0",
"mocha-webdriver": "0.2.9",
"moment-locales-webpack-plugin": "^1.2.0",
@ -72,11 +72,10 @@
"selenium-webdriver": "3.6.0",
"sinon": "7.1.1",
"source-map-loader": "^0.2.4",
"stats-webpack-plugin": "^0.7.0",
"tmp-promise": "1.0.5",
"typescript": "3.9.3",
"webpack": "4.41.0",
"webpack-cli": "3.3.2",
"typescript": "4.7.4",
"webpack": "5.73.0",
"webpack-cli": "4.10.0",
"why-is-node-running": "2.0.3"
},
"dependencies": {
@ -127,7 +126,6 @@
"mousetrap": "1.6.2",
"multiparty": "4.2.2",
"node-fetch": "2.2.0",
"numeral": "2.0.6",
"pg": "8.6.0",
"plotly.js-basic-dist": "1.51.1",
"popper-max-size-modifier": "0.2.0",

@ -1,144 +0,0 @@
from six.moves import xrange
def _is_array(obj):
return isinstance(obj, list)
def get(obj, path):
"""
Looks up and returns a path in the object. Returns None if the path isn't there.
"""
for part in path:
try:
obj = obj[part]
except(KeyError, IndexError):
return None
return obj
def glob(obj, path, func, extra_arg):
"""
Resolves wildcards in `path`, calling func for all matching paths. Returns the number of
times that func was called.
obj - An object to scan.
path - Path to an item in an object or an array in obj. May contain the special key '*', which
-- for arrays only -- means "for all indices".
func - Will be called as func(subobj, key, fullPath, extraArg).
extra_arg - An arbitrary value to pass along to func, for convenience.
Returns count of matching paths, for which func got called.
"""
return _globHelper(obj, path, path, func, extra_arg)
def _globHelper(obj, path, full_path, func, extra_arg):
for i, part in enumerate(path[:-1]):
if part == "*" and _is_array(obj):
# We got an array wildcard
subpath = path[i + 1:]
count = 0
for subobj in obj:
count += _globHelper(subobj, subpath, full_path, func, extra_arg)
return count
try:
obj = obj[part]
except:
raise Exception("gpath.glob: non-existent object at " +
describe(full_path[:len(full_path) - len(path) + i + 1]))
return func(obj, path[-1], full_path, extra_arg) or 1
def place(obj, path, value):
"""
Sets or deletes an object property in DocObj.
gpath - Path to an Object in obj.
value - Any value. Setting None will remove the selected object key.
"""
return glob(obj, path, _placeHelper, value)
def _placeHelper(subobj, key, full_path, value):
if not isinstance(subobj, dict):
raise Exception("gpath.place: not a plain object at " + describe(dirname(full_path)))
if value is not None:
subobj[key] = value
elif key in subobj:
del subobj[key]
def _checkIsArray(subobj, errPrefix, index, itemPath, isInsert):
"""
This is a helper for checking operations on arrays, and throwing descriptive errors.
"""
if subobj is None:
raise Exception(errPrefix + ": non-existent object at " + describe(dirname(itemPath)))
elif not _is_array(subobj):
raise Exception(errPrefix + ": not an array at " + describe(dirname(itemPath)))
else:
length = len(subobj)
validIndex = (isinstance(index, int) and index >= 0 and index < length)
validInsertIndex = (index is None or index == length)
if not (validIndex or (isInsert and validInsertIndex)):
raise Exception(errPrefix + ": invalid array index: " + describe(itemPath))
def insert(obj, path, value):
"""
Inserts an element into an array in DocObj.
gpath - Path to an item in an array in obj.
The new value will be inserted before the item pointed to by gpath.
The last component of gpath may be null, in which case the value is appended at the end.
value - Any value.
"""
return glob(obj, path, _insertHelper, value)
def _insertHelper(subobj, index, fullPath, value):
_checkIsArray(subobj, "gpath.insert", index, fullPath, True)
if index is None:
subobj.append(value)
else:
subobj.insert(index, value)
def update(obj, path, value):
"""
Updates an element in an array in DocObj.
gpath - Path to an item in an array in obj.
value - Any value.
"""
return glob(obj, path, _updateHelper, value)
def _updateHelper(subobj, index, fullPath, value):
if index == '*':
_checkIsArray(subobj, "gpath.update", None, fullPath, True)
for i in xrange(len(subobj)):
subobj[i] = value
return len(subobj)
else:
_checkIsArray(subobj, "gpath.update", index, fullPath, False)
subobj[index] = value
def remove(obj, path):
"""
Removes an element from an array in DocObj.
gpath - Path to an item in an array in obj.
"""
return glob(obj, path, _removeHelper, None)
def _removeHelper(subobj, index, fullPath, _):
_checkIsArray(subobj, "gpath.remove", index, fullPath, False)
del subobj[index]
def dirname(path):
"""
Returns path without the last component, like a directory name in a filesystem path.
"""
return path[:-1]
def basename(path):
"""
Returns the last component of path, like base name of a filesystem path.
"""
return path[-1] if path else None
def describe(path):
"""
Returns a human-readable representation of path.
"""
return "/" + "/".join(str(p) for p in path)

@ -1,159 +0,0 @@
import unittest
import gpath
class TestGpath(unittest.TestCase):
def setUp(self):
self.obj = {
"foo": [{"bar": 1}, {"bar": 2}, {"baz": 3}],
"hello": "world"
}
def test_get(self):
self.assertEqual(gpath.get(self.obj, ["foo", 0, "bar"]), 1)
self.assertEqual(gpath.get(self.obj, ["foo", 2]), {"baz": 3})
self.assertEqual(gpath.get(self.obj, ["hello"]), "world")
self.assertEqual(gpath.get(self.obj, []), self.obj)
self.assertEqual(gpath.get(self.obj, ["foo", 0, "baz"]), None)
self.assertEqual(gpath.get(self.obj, ["foo", 4]), None)
self.assertEqual(gpath.get(self.obj, ["foo", 4, "baz"]), None)
self.assertEqual(gpath.get(self.obj, [0]), None)
def test_set(self):
gpath.place(self.obj, ["foo"], {"bar": 1, "baz": 2})
self.assertEqual(self.obj["foo"], {"bar": 1, "baz": 2})
gpath.place(self.obj, ["foo", "bar"], 17)
self.assertEqual(self.obj["foo"], {"bar": 17, "baz": 2})
gpath.place(self.obj, ["foo", "baz"], None)
self.assertEqual(self.obj["foo"], {"bar": 17})
self.assertEqual(self.obj["hello"], "world")
gpath.place(self.obj, ["hello"], None)
self.assertFalse("hello" in self.obj)
gpath.place(self.obj, ["hello"], None) # OK to remove a non-existent property.
self.assertFalse("hello" in self.obj)
gpath.place(self.obj, ["hello"], "blah")
self.assertEqual(self.obj["hello"], "blah")
def test_set_strict(self):
with self.assertRaisesRegex(Exception, r"non-existent"):
gpath.place(self.obj, ["bar", 4], 17)
with self.assertRaisesRegex(Exception, r"not a plain object"):
gpath.place(self.obj, ["foo", 0], 17)
def test_insert(self):
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
gpath.insert(self.obj, ["foo", 0], "asdf")
self.assertEqual(self.obj["foo"], ["asdf", {"bar": 1}, {"bar": 2}, {"baz": 3}])
gpath.insert(self.obj, ["foo", 3], "hello")
self.assertEqual(self.obj["foo"], ["asdf", {"bar": 1}, {"bar": 2}, "hello", {"baz": 3}])
gpath.insert(self.obj, ["foo", None], "world")
self.assertEqual(self.obj["foo"],
["asdf", {"bar": 1}, {"bar": 2}, "hello", {"baz": 3}, "world"])
def test_insert_strict(self):
with self.assertRaisesRegex(Exception, r'not an array'):
gpath.insert(self.obj, ["foo"], "asdf")
with self.assertRaisesRegex(Exception, r'invalid.*index'):
gpath.insert(self.obj, ["foo", -1], 17)
with self.assertRaisesRegex(Exception, r'invalid.*index'):
gpath.insert(self.obj, ["foo", "foo"], 17)
def test_update(self):
"""update should update array items"""
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
gpath.update(self.obj, ["foo", 0], "asdf")
self.assertEqual(self.obj["foo"], ["asdf", {"bar": 2}, {"baz": 3}])
gpath.update(self.obj, ["foo", 2], "hello")
self.assertEqual(self.obj["foo"], ["asdf", {"bar": 2}, "hello"])
gpath.update(self.obj, ["foo", 1], None)
self.assertEqual(self.obj["foo"], ["asdf", None, "hello"])
def test_update_strict(self):
"""update should be strict"""
with self.assertRaisesRegex(Exception, r'non-existent'):
gpath.update(self.obj, ["bar", 4], 17)
with self.assertRaisesRegex(Exception, r'not an array'):
gpath.update(self.obj, ["foo"], 17)
with self.assertRaisesRegex(Exception, r'invalid.*index'):
gpath.update(self.obj, ["foo", -1], 17)
with self.assertRaisesRegex(Exception, r'invalid.*index'):
gpath.update(self.obj, ["foo", None], 17)
def test_remove(self):
"""remove should remove indices"""
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
gpath.remove(self.obj, ["foo", 0])
self.assertEqual(self.obj["foo"], [{"bar": 2}, {"baz": 3}])
gpath.remove(self.obj, ["foo", 1])
self.assertEqual(self.obj["foo"], [{"bar": 2}])
gpath.remove(self.obj, ["foo", 0])
self.assertEqual(self.obj["foo"], [])
def test_remove_strict(self):
"""remove should be strict"""
with self.assertRaisesRegex(Exception, r'non-existent'):
gpath.remove(self.obj, ["bar", 4])
with self.assertRaisesRegex(Exception, r'not an array'):
gpath.remove(self.obj, ["foo"])
with self.assertRaisesRegex(Exception, r'invalid.*index'):
gpath.remove(self.obj, ["foo", -1])
with self.assertRaisesRegex(Exception, r'invalid.*index'):
gpath.remove(self.obj, ["foo", None])
def test_glob(self):
"""glob should scan arrays"""
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
self.assertEqual(gpath.place(self.obj, ["foo", "*", "bar"], 17), 3)
self.assertEqual(self.obj["foo"], [{"bar": 17}, {"bar": 17}, {"baz": 3, "bar": 17}])
with self.assertRaisesRegex(Exception, r'non-existent object at \/foo\/\*\/bad'):
gpath.place(self.obj, ["foo", "*", "bad", "test"], 10)
self.assertEqual(gpath.update(self.obj, ["foo", "*"], "hello"), 3)
self.assertEqual(self.obj["foo"], ["hello", "hello", "hello"])
def test_glob_strict_wildcard(self):
"""should only support tail wildcard for updates"""
with self.assertRaisesRegex(Exception, r'invalid array index'):
gpath.remove(self.obj, ["foo", "*"])
with self.assertRaisesRegex(Exception, r'invalid array index'):
gpath.insert(self.obj, ["foo", "*"], 1)
def test_glob_wildcard_keys(self):
"""should not scan object keys"""
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
self.assertEqual(gpath.place(self.obj, ["foo", 0, "*"], 17), 1)
self.assertEqual(self.obj["foo"], [{"bar": 1, '*': 17}, {"bar": 2}, {"baz": 3}])
with self.assertRaisesRegex(Exception, r'non-existent'):
gpath.place(self.obj, ["*", 0, "bar"], 17)
def test_glob_nested(self):
"""should scan nested arrays"""
self.obj = [{"a": [1,2,3]}, {"a": [4,5,6]}, {"a": [7,8,9]}]
self.assertEqual(gpath.update(self.obj, ["*", "a", "*"], 5), 9)
self.assertEqual(self.obj, [{"a": [5,5,5]}, {"a": [5,5,5]}, {"a": [5,5,5]}])
def test_dirname(self):
"""dirname should return path without last component"""
self.assertEqual(gpath.dirname(["foo", "bar", "baz"]), ["foo", "bar"])
self.assertEqual(gpath.dirname([1, 2]), [1])
self.assertEqual(gpath.dirname(["foo"]), [])
self.assertEqual(gpath.dirname([]), [])
def test_basename(self):
"""basename should return the last component of path"""
self.assertEqual(gpath.basename(["foo", "bar", "baz"]), "baz")
self.assertEqual(gpath.basename([1, 2]), 2)
self.assertEqual(gpath.basename(["foo"]), "foo")
self.assertEqual(gpath.basename([]), None)
if __name__ == "__main__":
unittest.main()

@ -16,7 +16,7 @@ import {Client, ClientMethod} from 'app/server/lib/Client';
import {CommClientConnect} from 'app/common/CommTypes';
import {delay} from 'app/common/delay';
import {isLongerThan} from 'app/common/gutil';
import {fromCallback, getAvailablePort} from 'app/server/lib/serverUtils';
import {connect as connectSock, fromCallback, getAvailablePort, listenPromise} from 'app/server/lib/serverUtils';
import {Sessions} from 'app/server/lib/Sessions';
import * as testUtils from 'test/server/testUtils';
import * as session from '@gristlabs/express-session';
@ -52,7 +52,7 @@ describe('Comm', function() {
server = http.createServer();
comm = new Comm(server, {sessions});
comm.registerMethods(methods);
return fromCallback(cb => server.listen(0, 'localhost', cb));
return listenPromise(server.listen(0, 'localhost'));
}
async function stopComm() {
@ -500,8 +500,7 @@ export class TcpForwarder {
public async connect() {
await this.disconnect();
this._server = new Server((sock) => this._onConnect(sock));
await new Promise((resolve, reject) =>
this._server!.on('error', reject).listen(this.port, resolve));
await listenPromise(this._server.listen(this.port));
}
public async disconnectClientSide() {
await Promise.all(Array.from(this._connections.keys(), destroySock));
@ -528,9 +527,7 @@ export class TcpForwarder {
}
}
private async _onConnect(clientSock: Socket) {
const serverSock = new Socket();
await new Promise((resolve, reject) =>
serverSock.on('error', reject).connect(this._serverPort, resolve));
const serverSock = await connectSock(this._serverPort);
clientSock.pipe(serverSock);
serverSock.pipe(clientSock);
clientSock.on('error', (err) => serverSock.destroy(err));

@ -1,5 +1,5 @@
import {getAppRoot} from 'app/server/lib/places';
import {fromCallback} from 'app/server/lib/serverUtils';
import {fromCallback, listenPromise} from 'app/server/lib/serverUtils';
import * as express from 'express';
import * as http from 'http';
import {AddressInfo, Socket} from 'net';
@ -45,7 +45,7 @@ export function serveCustomViews(): Promise<Serving> {
export async function serveSomething(setup: (app: express.Express) => void, port= 0): Promise<Serving> {
const app = express();
const server = http.createServer(app);
await fromCallback(cb => server.listen(port, cb));
await listenPromise(server.listen(port));
const connections = new Set<Socket>();
server.on('connection', (conn) => {

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save