Skip to content
This repository has been archived by the owner on Oct 7, 2024. It is now read-only.

refactor: move internal state from objects to maps #41

Merged
merged 3 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 80.95,
functions: 88.88,
lines: 82.6,
statements: 82.75,
branches: 77.77,
functions: 84.84,
lines: 79.31,
statements: 79.31,
},
},

Expand Down
65 changes: 65 additions & 0 deletions src/CaseInsensitiveMap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { CaseInsensitiveMap } from './CaseInsensitiveMap';

describe('CaseInsensitiveMap', () => {
it('should set and get values case-insensitively', () => {
const map = new CaseInsensitiveMap<number>();
map.set('foo', 1);
map.set('BAR', 2);
expect(map.get('foo')).toBe(1);
expect(map.get('FOO')).toBe(1);
expect(map.get('bar')).toBe(2);
expect(map.get('BaR')).toBe(2);
});

it('should check for keys case-insensitively', () => {
const map = new CaseInsensitiveMap<number>();
map.set('foo', 1);
expect(map.has('foo')).toBe(true);
expect(map.has('FOO')).toBe(true);
expect(map.has('bar')).toBe(false);
expect(map.has('BaR')).toBe(false);
});

it('should delete keys case-insensitively', () => {
const map = new CaseInsensitiveMap<number>();
map.set('foo', 1);
map.set('BAR', 2);
expect(map.delete('foo')).toBe(true);
expect(map.has('foo')).toBe(false);
expect(map.has('FOO')).toBe(false);
expect(map.has('bar')).toBe(true);
expect(map.has('BaR')).toBe(true);
expect(map.delete('BaR')).toBe(true);
expect(map.has('bar')).toBe(false);
expect(map.has('BaR')).toBe(false);
});

it('should be able to construct from items', () => {
const map = new CaseInsensitiveMap<number>([
['foo', 1],
['BAR', 2],
]);
expect(map.get('foo')).toBe(1);
expect(map.get('FOO')).toBe(1);
expect(map.get('bar')).toBe(2);
expect(map.get('BaR')).toBe(2);
});

it('should convert the map to an object', () => {
const map = new CaseInsensitiveMap<number>([
['foo', 1],
['BAR', 2],
]);
const obj = map.toObject();
expect(obj).toStrictEqual({ foo: 1, bar: 2 });
});

it('should create a map from an object', () => {
const obj = { foo: 1, BAR: 2 };
const map = CaseInsensitiveMap.fromObject(obj);
expect(map.get('foo')).toBe(1);
expect(map.get('FOO')).toBe(1);
expect(map.get('bar')).toBe(2);
expect(map.get('BaR')).toBe(2);
});
});
25 changes: 25 additions & 0 deletions src/CaseInsensitiveMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export class CaseInsensitiveMap<T> extends Map<string, T> {
static fromObject<T>(obj: Record<string, T>): CaseInsensitiveMap<T> {
return new CaseInsensitiveMap(Object.entries(obj));
}

toObject(): Record<string, T> {
return Object.fromEntries(this.entries());
}

get(key: string): T | undefined {
return super.get(key.toLowerCase());
}

has(key: string): boolean {
return super.has(key.toLowerCase());
}

set(key: string, value: T): this {
return super.set(key.toLowerCase(), value);
}

delete(key: string): boolean {
return super.delete(key.toLowerCase());
}
}
74 changes: 34 additions & 40 deletions src/snap-keyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import EventEmitter from 'events';
import { assert, object, string, record, Infer } from 'superstruct';
import { v4 as uuid } from 'uuid';

import { CaseInsensitiveMap } from './CaseInsensitiveMap';
import { SnapMessage, SnapMessageStruct } from './types';
import { DeferredPromise, strictMask, toJson, unique } from './util';

Expand All @@ -33,19 +34,19 @@ export class SnapKeyring extends EventEmitter {

#snapClient: KeyringSnapControllerClient;

#addressToAccount: Record<string, KeyringAccount>;
#addressToAccount: CaseInsensitiveMap<KeyringAccount>;

#addressToSnapId: Record<string, string>;
#addressToSnapId: CaseInsensitiveMap<string>;

#pendingRequests: Record<string, DeferredPromise<any>>;
#pendingRequests: CaseInsensitiveMap<DeferredPromise<any>>;

constructor(controller: SnapController) {
super();
this.type = SnapKeyring.type;
this.#snapClient = new KeyringSnapControllerClient({ controller });
this.#addressToAccount = {};
this.#addressToSnapId = {};
this.#pendingRequests = {};
this.#addressToAccount = new CaseInsensitiveMap();
this.#addressToSnapId = new CaseInsensitiveMap();
this.#pendingRequests = new CaseInsensitiveMap();
}

/**
Expand Down Expand Up @@ -73,9 +74,8 @@ export class SnapKeyring extends EventEmitter {
// Don't call the snap back to list the accounts. The main use case for
// this method is to allow the snap to verify if the keyring's state is
// in sync with the snap's state.
return Object.values(this.#addressToAccount).filter(
(account) =>
this.#addressToSnapId[account.address.toLowerCase()] === snapId,
return Array.from(this.#addressToAccount.values()).filter(
(account) => this.#addressToSnapId.get(account.address) === snapId,
);
}

Expand All @@ -97,8 +97,8 @@ export class SnapKeyring extends EventEmitter {
*/
async serialize(): Promise<KeyringState> {
return {
addressToAccount: this.#addressToAccount,
addressToSnapId: this.#addressToSnapId,
addressToAccount: this.#addressToAccount.toObject(),
addressToSnapId: this.#addressToSnapId.toObject(),
};
}

Expand All @@ -108,26 +108,19 @@ export class SnapKeyring extends EventEmitter {
* @param state - Serialized keyring state.
*/
async deserialize(state: KeyringState | undefined): Promise<void> {
// If the state is undefined, it means that this is a new keyring, so we
// don't need to do anything.
if (state === undefined) {
return;
}

assert(state, KeyringStateStruct);
this.#addressToAccount = state.addressToAccount;
this.#addressToSnapId = state.addressToSnapId;

// Normalize addresses to lower case.
for (const address of Object.keys(this.#addressToAccount)) {
const account = this.#addressToAccount[address];
const snapId = this.#addressToSnapId[address];
if (account !== undefined && snapId !== undefined) {
// Account object
delete this.#addressToAccount[address];
this.#addressToAccount[address.toLowerCase()] = account;
// Snap ID
delete this.#addressToSnapId[address];
this.#addressToSnapId[address.toLowerCase()] = snapId;
}
}
this.#addressToAccount = CaseInsensitiveMap.fromObject(
state.addressToAccount,
);
this.#addressToSnapId = CaseInsensitiveMap.fromObject(
state.addressToSnapId,
);
}

/**
Expand All @@ -138,7 +131,9 @@ export class SnapKeyring extends EventEmitter {
async getAccounts(): Promise<string[]> {
// Do not call the snap here. This method is called by the UI, keep it
// _fast_.
return unique(Object.values(this.#addressToAccount).map((a) => a.address));
return unique(
Array.from(this.#addressToAccount.values(), (account) => account.address),
);
}

/**
Expand Down Expand Up @@ -172,7 +167,7 @@ export class SnapKeyring extends EventEmitter {
}

const promise = new DeferredPromise<Response>();
this.#pendingRequests[id] = promise;
this.#pendingRequests.set(id, promise);
return promise.promise;
}

Expand Down Expand Up @@ -310,7 +305,7 @@ export class SnapKeyring extends EventEmitter {
*/
async #syncAllSnapsAccounts(...extraSnapIds: string[]): Promise<void> {
const snapIds = unique(
Object.values(this.#addressToSnapId).concat(extraSnapIds),
Array.from(this.#addressToSnapId.values()).concat(extraSnapIds),
);

for (const snapId of snapIds) {
Expand Down Expand Up @@ -357,8 +352,8 @@ export class SnapKeyring extends EventEmitter {
account: KeyringAccount;
snapId: string;
} {
const account = this.#addressToAccount[address.toLowerCase()];
const snapId = this.#addressToSnapId[address.toLowerCase()];
const account = this.#addressToAccount.get(address);
const snapId = this.#addressToSnapId.get(address);
if (snapId === undefined || account === undefined) {
throw new Error(`Account address not found: ${address}`);
}
Expand All @@ -372,13 +367,13 @@ export class SnapKeyring extends EventEmitter {
* @param result - Result of the request.
*/
#resolveRequest(id: string, result: any): void {
const signingPromise = this.#pendingRequests[id];
const signingPromise = this.#pendingRequests.get(id);
if (signingPromise?.resolve === undefined) {
console.warn(`No pending request found for ID: ${id}`);
return;
}

delete this.#pendingRequests[id];
this.#pendingRequests.delete(id);
signingPromise.resolve(result);
}

Expand All @@ -390,8 +385,7 @@ export class SnapKeyring extends EventEmitter {
*/
#getAccountsBySnapId(snapId: string): KeyringAccount[] {
return Object.values(this.#addressToAccount).filter(
(account) =>
this.#addressToSnapId[account.address.toLowerCase()] === snapId,
(account) => this.#addressToSnapId.get(account.address) === snapId,
);
}

Expand All @@ -402,8 +396,8 @@ export class SnapKeyring extends EventEmitter {
* @param snapId - The snap ID of the account.
*/
#addAccountToMaps(account: KeyringAccount, snapId: string): void {
this.#addressToAccount[account.address.toLowerCase()] = account;
this.#addressToSnapId[account.address.toLowerCase()] = snapId;
this.#addressToAccount.set(account.address, account);
this.#addressToSnapId.set(account.address, snapId);
}

/**
Expand All @@ -412,7 +406,7 @@ export class SnapKeyring extends EventEmitter {
* @param account - The account to be removed.
*/
#removeAccountFromMaps(account: KeyringAccount): void {
delete this.#addressToAccount[account.address.toLowerCase()];
delete this.#addressToSnapId[account.address.toLowerCase()];
this.#addressToAccount.delete(account.address);
this.#addressToSnapId.delete(account.address);
}
}