Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"lib": ["dom", "dom.iterable", "esnext"],
"jsx": "react-jsx",
"strictBindCallApply": true,
"noImplicitThis": true,
"noEmitHelpers": true,
"resolveJsonModule": true,
"skipLibCheck": true,
Expand Down
25 changes: 24 additions & 1 deletion web/packages/shared/utils/highbar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { arrayObjectIsEqual, equalsDeep, mergeDeep } from './highbar';
import { arrayObjectIsEqual, equalsDeep, mergeDeep, runOnce } from './highbar';

describe('mergeDeep can merge two', () => {
it('objects together', () => {
Expand Down Expand Up @@ -368,3 +368,26 @@ describe('equalsDeep', () => {
).toBe(false);
});
});

test('runOnce only runs once and preserves "this"', () => {
class Counter {
count = 0;

increment = runOnce(
// Use 'this' in a convoluted way to make sure it's passed correctly.
function (this: Counter) {
this.count++;
}
);
}

const c = new Counter();

// First call increments (and doesn't throw because of 'this' being undefined).
expect(() => c.increment()).not.toThrow();
expect(c.count).toBe(1);

// Subsequent calls do not increment.
c.increment();
expect(c.count).toBe(1);
});
194 changes: 13 additions & 181 deletions web/packages/shared/utils/highbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,22 +133,18 @@ export function isObject(checkVal: unknown): checkVal is object {
return checkVal != null && (type == 'object' || type == 'function');
}

/**
* Lodash <https://lodash.com/>
* Copyright JS Foundation and other contributors <https://js.foundation/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
export function runOnce<T extends (...args) => any>(func: T) {
let n = 2;
let result;
return function () {
if (--n > 0) {
// This implementation does not pass strictBindCallApply check.
result = func.apply(this, arguments as any);
}
if (n <= 1) {
/** Runs the provided function only once. */
export function runOnce<T extends (...args: any[]) => any>(func: T) {
let hasRun = false;
let result: ReturnType<T>;

// 'this' is a special TS parameter that is erased during compilation
// https://www.typescriptlang.org/docs/handbook/2/classes.html#this-parameters
return function (this: unknown, ...args: Parameters<T>) {
if (!hasRun) {
hasRun = true;
result = func.apply(this, args);
// Remove the reference, so it can be garbage-collected.
func = undefined;
}
return result;
Expand Down Expand Up @@ -305,7 +301,7 @@ export function debounce<T extends (...args: any) => any>(
return timerId === undefined ? result : trailingEdge(Date.now());
}

function debounced() {
function debounced(this: unknown) {
var time = Date.now(),
isInvoking = shouldInvoke(time);

Expand Down Expand Up @@ -333,167 +329,3 @@ export function debounce<T extends (...args: any) => any>(
debounced.flush = flush;
return debounced;
}

interface MapCacheType {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to get rid of memoize because it had many this issues.
It was only used in one place, which was trivial to refactor, and this old-style code was hard to maintain (we already had a few any and TS/ESLint ignores there).

delete(key: any): boolean;
get(key: any): any;
has(key: any): boolean;
set(key: any, value: any): this;
clear?: (() => void) | undefined;
}

type MemoizedFunction = {
cache: MapCacheType;
};

/**
* Lodash <https://lodash.com/>
* Copyright JS Foundation and other contributors <https://js.foundation/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
export function memoize<T extends (...args: any) => any>(
func: T
): T & MemoizedFunction {
const memoized = function () {
const args = arguments;
const key = args[0];
const cache = memoized.cache;

if (cache.has(key)) {
return cache.get(key);
}
// `as any` because the implementation does not pass strictBindCallApply check.
const result = func.apply(this, args as any);
memoized.cache = cache.set(key, result) || cache;
return result;
};
memoized.cache = new (memoize.Cache || MapCache)();
/* eslint-disable @typescript-eslint/ban-ts-comment*/
// @ts-ignore
return memoized;
}

// Expose `MapCache`.
memoize.Cache = MapCache;

function MapCache(entries?: any) {
let index = -1;
const length = entries == null ? 0 : entries.length;

this.clear();
while (++index < length) {
const entry = entries[index];
this.set(entry[0], entry[1]);
}
}

function mapCacheClear() {
this.size = 0;
this.__data__ = {
hash: new Hash(),
map: new Map(),
string: new Hash(),
};
}

function mapCacheDelete(key) {
var result = getMapData(this, key)['delete'](key);
this.size -= result ? 1 : 0;
return result;
}

function mapCacheGet(key) {
return getMapData(this, key).get(key);
}

function mapCacheHas(key) {
return getMapData(this, key).has(key);
}

function mapCacheSet(key, value) {
var data = getMapData(this, key),
size = data.size;

data.set(key, value);
this.size += data.size == size ? 0 : 1;
return this;
}

MapCache.prototype.clear = mapCacheClear;
MapCache.prototype['delete'] = mapCacheDelete;
MapCache.prototype.get = mapCacheGet;
MapCache.prototype.has = mapCacheHas;
MapCache.prototype.set = mapCacheSet;

function Hash(entries?) {
var index = -1,
length = entries == null ? 0 : entries.length;

this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}

const HASH_UNDEFINED = '__lodash_hash_undefined__';

function hashClear() {
this.__data__ = Object.create ? Object.create(null) : {};
this.size = 0;
}

function hashDelete(key) {
var result = this.has(key) && delete this.__data__[key];
this.size -= result ? 1 : 0;
return result;
}

function hashGet(key) {
var data = this.__data__;
if (Object.create) {
var result = data[key];
return result === HASH_UNDEFINED ? undefined : result;
}
return Object.hasOwnProperty.call(data, key) ? data[key] : undefined;
}

function hashHas(key) {
var data = this.__data__;
return Object.create
? data[key] !== undefined
: Object.hasOwnProperty.call(data, key);
}

function hashSet(key, value) {
var data = this.__data__;
this.size += this.has(key) ? 0 : 1;
data[key] = Object.create && value === undefined ? HASH_UNDEFINED : value;
return this;
}

// Add methods to `Hash`.
Hash.prototype.clear = hashClear;
Hash.prototype['delete'] = hashDelete;
Hash.prototype.get = hashGet;
Hash.prototype.has = hashHas;
Hash.prototype.set = hashSet;

function getMapData(map, key) {
var data = map.__data__;
return isKeyable(key)
? data[typeof key == 'string' ? 'string' : 'hash']
: data.map;
}

function isKeyable(value) {
var type = typeof value;
return type == 'string' ||
type == 'number' ||
type == 'symbol' ||
type == 'boolean'
? value !== '__proto__'
: value === null;
}
14 changes: 9 additions & 5 deletions web/packages/teleport/src/services/websession/websession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ const logger = Logger.create('services/session');
let sesstionCheckerTimerId = null;

const session = {
_isRenewing: false,
_isDeviceTrustRequired: false,
_isDeviceTrusted: false,

logout(rememberLocation = false) {
let samlSloUrl = '';

Expand Down Expand Up @@ -204,29 +208,29 @@ const session = {
});
},

_setAndBroadcastIsRenewing(value) {
_setAndBroadcastIsRenewing(value: boolean) {
this._setIsRenewing(value);
storageService.broadcast(KeysEnum.TOKEN_RENEW, value);
},

_setIsRenewing(value) {
_setIsRenewing(value: boolean) {
this._isRenewing = value;
},

_getIsRenewing() {
return !!this._isRenewing;
return this._isRenewing;
},

setDeviceTrustRequired() {
this._isDeviceTrustRequired = true;
},

getDeviceTrustRequired() {
return !!this._isDeviceTrustRequired;
return this._isDeviceTrustRequired;
},

getIsDeviceTrusted() {
return !!this._isDeviceTrusted;
return this._isDeviceTrusted;
},

// a session will never be "downgraded" so we can just set to true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,19 @@

import { spawn } from 'child_process';

import { memoize } from 'shared/utils/highbar';

import Logger from 'teleterm/logger';
import { unique } from 'teleterm/ui/utils/uid';

const logger = new Logger('resolveShellEnv()');
const logger = new Logger('resolveShellEnv');
const resolveShellMaxTime = 10_000; // 10s

export const resolveShellEnvCached = memoize(resolveShellEnv);
const cache = new Map<string, Promise<typeof process.env | undefined>>();
export function resolveShellEnvCached(shell: string) {
if (!cache.has(shell)) {
cache.set(shell, resolveShellEnv(shell));
}
return cache.get(shell);
}

export class ResolveShellEnvTimeoutError extends Error {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,6 @@ async function setUpPtyProcess(
refreshTitle();
});

// TODO(ravicious): Refactor runOnce to not use the `n` variable. Otherwise runOnce subtracts 1
// from n each time the resulting function is executed, which in this context means each time data
// is transferred from PTY.
const markDocumentAsConnectedOnce = runOnce(() => {
if ('status' in doc) {
documentsService.update(doc.uri, { status: 'connected' });
Expand Down
Loading