Use efficient filters

This commit is contained in:
Andras Schmelczer 2025-12-07 11:30:19 +00:00
parent 07cb8491e2
commit 3f2ecfb0b6
13 changed files with 82 additions and 47 deletions

View file

@ -55,6 +55,25 @@ export default [
message: "Use replaceAll instead of replace to replace all occurrences of a substring."
}
],
"no-restricted-syntax": [
"error",
{
selector: "CallExpression[callee.property.name='splice'][arguments.length=2][arguments.1.type='Literal'][arguments.1.value=1]",
message: "Use `removeFromArray(array, item)` instead of manually using indexOf + splice(index, 1). Import from 'sync-client/src/utils/remove-from-array'."
},
{
selector: "CallExpression[callee.property.name='filter'] > ArrowFunctionExpression[body.type='BinaryExpression'][body.operator='!==']",
message: "Use `removeFromArray(array, item)` instead of filter(x => x !== item) for better performance. Import from 'sync-client/src/utils/remove-from-array'."
},
{
selector: "CallExpression[callee.property.name='filter'] > ArrowFunctionExpression > BlockStatement > ReturnStatement > BinaryExpression[operator='!==']",
message: "Use `removeFromArray(array, item)` instead of filter(x => { return x !== item }) for better performance. Import from 'sync-client/src/utils/remove-from-array'."
},
{
selector: "CallExpression[callee.property.name='filter'] > FunctionExpression[body.type='BlockStatement'] > BlockStatement > ReturnStatement > BinaryExpression[operator='!==']",
message: "Use `removeFromArray(array, item)` instead of filter(function(x) { return x !== item }) for better performance. Import from 'sync-client/src/utils/remove-from-array'."
}
],
"unused-imports/no-unused-vars": [
"warn",
{

View file

@ -5,13 +5,14 @@ import type {
NetworkConnectionStatus,
SyncClient
} from "sync-client";
import { utils } from "sync-client";
export class StatusDescription {
private lastHistoryStats: HistoryStats | undefined;
private lastRemaining: number | undefined;
private lastConnectionState: NetworkConnectionStatus | undefined;
private statusChangeListeners: (() => unknown)[] = [];
private readonly statusChangeListeners: (() => unknown)[] = [];
public constructor(private readonly syncClient: SyncClient) {
void this.updateConnectionState();
@ -46,9 +47,7 @@ export class StatusDescription {
this.statusChangeListeners.push(listener);
}
public removeStatusChangeListener(listener: () => unknown): void {
this.statusChangeListeners = this.statusChangeListeners.filter(
(l) => l !== listener
);
utils.removeFromArray(this.statusChangeListeners, listener);
}
public renderStatusDescription(container: HTMLElement): void {

View file

@ -5,6 +5,7 @@ import { slowWebSocketFactory } from "./utils/debugging/slow-web-socket-factory"
import { getRandomColor } from "./utils/get-random-color";
import { lineAndColumnToPosition } from "./utils/line-and-column-to-position";
import { positionToLineAndColumn } from "./utils/position-to-line-and-column";
import { removeFromArray } from "./utils/remove-from-array";
export {
SyncType,
@ -43,5 +44,6 @@ export const utils = {
getRandomColor,
positionToLineAndColumn,
lineAndColumnToPosition,
awaitAll
awaitAll,
removeFromArray
};

View file

@ -2,6 +2,7 @@ import type { Logger } from "../tracing/logger";
import { EMPTY_HASH } from "../utils/hash";
import { CoveredValues } from "../utils/data-structures/min-covered";
import { awaitAll } from "../utils/await-all";
import { removeFromArray } from "../utils/remove-from-array";
export type VaultUpdateId = number;
export type DocumentId = string;
@ -93,6 +94,7 @@ export class Database {
public get resolvedDocuments(): DocumentRecord[] {
const paths = new Map<string, DocumentRecord[]>();
this.documents
// eslint-disable-next-line no-restricted-syntax -- Type narrowing, not removing a specific item
.filter(({ metadata }) => metadata !== undefined)
.forEach((record) =>
paths.set(record.relativePath, [
@ -151,12 +153,12 @@ export class Database {
return;
}
entry.updates = entry.updates.filter((update) => update !== promise);
removeFromArray(entry.updates, promise);
// No need to save as Promises don't get serialized
}
public removeDocument(find: DocumentRecord): void {
this.documents = this.documents.filter((document) => document !== find);
removeFromArray(this.documents, find);
this.saveInTheBackground();
}

View file

@ -1,6 +1,7 @@
import type { Logger } from "../tracing/logger";
import { awaitAll } from "../utils/await-all";
import { Lock } from "../utils/data-structures/locks";
import { removeFromArray } from "../utils/remove-from-array";
export interface SyncSettings {
remoteUri: string;
@ -69,10 +70,7 @@ export class Settings {
public removeOnSettingsChangeListener(
listener: (settings: SyncSettings, oldSettings: SyncSettings) => unknown
): void {
const index = this.onSettingsChangeHandlers.indexOf(listener);
if (index !== -1) {
this.onSettingsChangeHandlers.splice(index, 1);
}
removeFromArray(this.onSettingsChangeHandlers, listener);
}
public async setSetting<T extends keyof SyncSettings>(

View file

@ -8,6 +8,7 @@ import { createPromise } from "../utils/create-promise";
import type { WebSocketVaultUpdate } from "./types/WebSocketVaultUpdate";
import { awaitAll } from "../utils/await-all";
import { WEBSOCKET_DISCONNECT_TIMEOUT_IN_S } from "../consts";
import { removeFromArray } from "../utils/remove-from-array";
export class WebSocketManager {
private readonly webSocketStatusChangeListeners: ((
@ -227,12 +228,10 @@ export class WebSocketManager {
);
})
.finally(() => {
const index = this.outstandingPromises.indexOf(
removeFromArray(
this.outstandingPromises,
messageHandlingPromise
);
if (index !== -1) {
void this.outstandingPromises.splice(index, 1); // ignore the returned promise
}
});
void this.outstandingPromises.push(messageHandlingPromise); // ignore the returned promise

View file

@ -1,4 +1,5 @@
import type { RelativePath } from "../persistence/database";
import { removeFromArray } from "../utils/remove-from-array";
export class FileChangeNotifier {
private readonly listeners: ((filePath: RelativePath) => unknown)[] = [];
@ -12,10 +13,7 @@ export class FileChangeNotifier {
public removeFileChangeListener(
listener: (filePath: RelativePath) => unknown
): void {
const index = this.listeners.indexOf(listener);
if (index !== -1) {
this.listeners.splice(index, 1);
}
removeFromArray(this.listeners, listener);
}
public notifyOfFileChange(filePath: RelativePath): void {

View file

@ -444,11 +444,13 @@ export class Syncer {
);
if (originalFile !== undefined) {
// `originalFile` hasn't been deleted but it got moved instead
/* eslint-disable no-restricted-syntax -- Comparing by property, not direct equality */
locallyPossiblyDeletedFiles =
locallyPossiblyDeletedFiles.filter(
(item) =>
item.relativePath !== originalFile.relativePath
);
/* eslint-enable no-restricted-syntax */
this.logger.debug(
`Document '${originalFile.relativePath}' was not found under its current path in the database but was found under a different path (${relativePath}), scheduling sync to move it`

View file

@ -1,4 +1,5 @@
import { MAX_LOG_MESSAGE_COUNT } from "../consts";
import { removeFromArray } from "../utils/remove-from-array";
export enum LogLevel {
DEBUG = "DEBUG",
@ -63,10 +64,7 @@ export class Logger {
public removeOnMessageListener(
listener: (message: LogLine) => unknown
): void {
const index = this.onMessageListeners.indexOf(listener);
if (index !== -1) {
this.onMessageListeners.splice(index, 1);
}
removeFromArray(this.onMessageListeners, listener);
}
public reset(): void {

View file

@ -4,6 +4,7 @@ import {
} from "../consts";
import type { RelativePath } from "../persistence/database";
import type { Logger } from "./logger";
import { removeFromArray } from "../utils/remove-from-array";
export interface SyncCreateDetails {
type: SyncType.CREATE;
@ -68,7 +69,7 @@ export interface HistoryStats {
}
export class SyncHistory {
private _entries: HistoryEntry[] = [];
private readonly _entries: HistoryEntry[] = [];
private readonly syncHistoryUpdateListeners: ((
status: HistoryStats
@ -99,7 +100,7 @@ export class SyncHistory {
const candidate = this.findSimilarRecentUpdateEntry(historyEntry);
if (candidate !== undefined) {
this._entries = this._entries.filter((e) => e !== candidate);
removeFromArray(this._entries, candidate);
}
// Insert the entry at the beginning
@ -122,10 +123,7 @@ export class SyncHistory {
public removeSyncHistoryUpdateListener(
listener: (stats: HistoryStats) => unknown
): void {
const index = this.syncHistoryUpdateListeners.indexOf(listener);
if (index !== -1) {
this.syncHistoryUpdateListeners.splice(index, 1);
}
removeFromArray(this.syncHistoryUpdateListeners, listener);
}
public reset(): void {

View file

@ -2,17 +2,20 @@ import { makeRe } from "minimatch";
import type { Logger } from "../tracing/logger";
export function globsToRegexes(globs: string[], logger: Logger): RegExp[] {
return globs
.map((pattern) => {
const result = makeRe(pattern, {
dot: true
});
if (result === false) {
logger.warn(
`Failed to parse ${pattern}' as a glob pattern, skipping it`
);
}
return result;
})
.filter((pattern) => pattern !== false);
return (
globs
.map((pattern) => {
const result = makeRe(pattern, {
dot: true
});
if (result === false) {
logger.warn(
`Failed to parse ${pattern}' as a glob pattern, skipping it`
);
}
return result;
})
// eslint-disable-next-line no-restricted-syntax -- Filtering out false values, not removing a specific item
.filter((pattern) => pattern !== false)
);
}

View file

@ -0,0 +1,17 @@
/**
* Efficiently removes a specific item from an array by modifying it in place.
* This is more efficient than using `.filter(item => item !== toRemove)` as it avoids creating a new array
*
* @param array The array to modify
* @param item The item to remove
* @returns true if the item was found and removed, false otherwise
*/
export function removeFromArray<T>(array: T[], item: T): boolean {
const index = array.indexOf(item);
if (index !== -1) {
// eslint-disable-next-line no-restricted-syntax -- This is the implementation of the helper itself
array.splice(index, 1);
return true;
}
return false;
}

View file

@ -15,7 +15,7 @@ export class MockAgent extends MockClient {
private readonly pendingActions: Promise<unknown>[] = [];
// The renamed file finding algorithm isn't too smart so we can't both update and rename the same file
private doNotTouchWhileOffline: string[] = [];
private readonly doNotTouchWhileOffline: string[] = [];
public constructor(
initialSettings: Partial<SyncSettings>,
@ -54,10 +54,10 @@ export class MockAgent extends MockClient {
);
if (historyEntry) {
this.doNotTouchWhileOffline =
this.doNotTouchWhileOffline.filter(
(file) => file !== historyEntry[1]
);
utils.removeFromArray(
this.doNotTouchWhileOffline,
historyEntry[1]
);
}
switch (logLine.level) {
case LogLevel.ERROR: