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

Commit

Permalink
refactor: move internal state from objects to maps (#41)
Browse files Browse the repository at this point in the history
* feat: add `CaseInsensitiveMap`

* chore: use new map in `SnapKeyring`

* chore: lower coverage (improved in next PR)
  • Loading branch information
danroc authored Jul 4, 2023
1 parent a2abf05 commit 214614a
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 44 deletions.
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);
}
}

0 comments on commit 214614a

Please sign in to comment.