Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: avoid potential race-condition in fuel connectors init #2930

Merged
merged 19 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions .changeset/cold-masks-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/account": patch
---

fix: avoid potential race-condition in fuel connectors init
32 changes: 19 additions & 13 deletions apps/docs-snippets/src/guide/wallets/connectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,23 @@ class WalletConnector extends FuelConnector {
*/
describe('connectors', () => {
describe('instantiation', () => {
test('should be able to instantiate a Fuel SDK', () => {
test('should be able to instantiate a Fuel SDK', async () => {
// #region fuel-instantiation-1
// #import { Fuel };

const sdk = new Fuel();

/*
Awaits for initialization to mitigate potential race conditions
derived from the async nature of instantiating a connector.
*/
await sdk.init();
// #endregion fuel-instantiation-1

expect(sdk).toBeDefined();
});

test('should be able to instantiate with connectors', () => {
test('should be able to instantiate with connectors', async () => {
const defaultConnectors = (_opts: { devMode: boolean }): Array<FuelConnector> => [
new WalletConnector(),
];
Expand All @@ -159,29 +165,29 @@ describe('connectors', () => {
// #import { Fuel };
// #context import { defaultConnectors } from '@fuels/connectors';

const sdk = new Fuel({
const sdk = await new Fuel({
connectors: defaultConnectors({
devMode: true,
}),
});
}).init();
// #endregion fuel-options-connectors

expect(sdk).toBeDefined();
});

test('should be able to instantiate with memory storage', () => {
test('should be able to instantiate with memory storage', async () => {
// #region fuel-options-storage-memory
// #import { Fuel, MemoryStorage };

const sdk = new Fuel({
const sdk = await new Fuel({
storage: new MemoryStorage(),
});
}).init();
// #endregion fuel-options-storage-memory

expect(sdk).toBeDefined();
});

test('should be able to instantiate with local storage', () => {
test('should be able to instantiate with local storage', async () => {
const window = {
localStorage: {
setItem: vi.fn(),
Expand All @@ -194,25 +200,25 @@ describe('connectors', () => {
// #region fuel-options-storage-local
// #import { Fuel, LocalStorage };

const sdk = new Fuel({
const sdk = await new Fuel({
storage: new LocalStorage(window.localStorage),
});
}).init();
// #endregion fuel-options-storage-local

expect(sdk).toBeDefined();
});

test('should be able to instantiate with targetObject', () => {
test('should be able to instantiate with targetObject', async () => {
const window = {} as unknown as TargetObject;

// #region fuel-options-target-object
// #import { Fuel, TargetObject };

const targetObject: TargetObject = window || document;

const sdk = new Fuel({
const sdk = await new Fuel({
targetObject,
});
}).init();
// #endregion fuel-options-target-object

expect(sdk).toBeDefined();
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/src/guide/wallets/connectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,9 @@ It can be instantiated as follows:

<<< @/../../docs-snippets/src/guide/wallets/connectors.test.ts#fuel-instantiation-1{ts:line-numbers}

> [!NOTE] Note
> We recommend initializing the Fuel class with the `init` method to avoid any potential race conditions that may arise from the async nature of instantiating a connector.

### Options

Several options can be passed to the `Fuel` connector manager:
Expand Down
23 changes: 17 additions & 6 deletions packages/account/src/connectors/fuel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@ export class Fuel extends FuelConnector implements FuelSdk {
private _connectors: Array<FuelConnector> = [];
private _targetObject: TargetObject | null = null;
private _unsubscribes: Array<() => void> = [];
private _targetUnsubscribe: () => void;
private _targetUnsubscribe = () => {};
private _pingCache: CacheFor = {};
private _currentConnector?: FuelConnector | null;
private _initializationPromise: Promise<void> | null = null;

constructor(config: FuelConfig = Fuel.defaultConfig) {
super();
Expand All @@ -101,11 +102,21 @@ export class Fuel extends FuelConnector implements FuelSdk {
this._storage = config.storage === undefined ? this.getStorage() : config.storage;
// Setup all methods
this.setupMethods();
// Get the current connector from the storage
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.setDefaultConnector();
// Setup new connector listener for global events
this._targetUnsubscribe = this.setupConnectorListener();
this._initializationPromise = this.initialize();
}

private async initialize(): Promise<void> {
try {
await this.setDefaultConnector();
this._targetUnsubscribe = this.setupConnectorListener();
} catch (error) {
throw new FuelError(ErrorCode.INVALID_PROVIDER, 'Error initializing Fuel Connector');
}
}

public async init(): Promise<Fuel> {
nedsalk marked this conversation as resolved.
Show resolved Hide resolved
await this._initializationPromise;
return this;
}

/**
Expand Down
28 changes: 14 additions & 14 deletions packages/account/test/fuel-wallet-connector.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ describe('Fuel Connector on browser', () => {
});

it('should add connector using window events', async () => {
const fuel = new Fuel({
const fuel = await new Fuel({
connectors: [],
storage: null,
});
}).init();
let connectors = await fuel.connectors();
expect(connectors.length).toBe(0);

Expand Down Expand Up @@ -49,18 +49,18 @@ describe('Fuel Connector on browser', () => {
const thirdPartyConnector = new MockConnector({
name: 'Third Party Wallet',
});
const fuel = new Fuel({
const fuel = await new Fuel({
connectors: [walletConnector, thirdPartyConnector],
storage,
});
}).init();

// Select third party connector
await fuel.selectConnector(thirdPartyConnector.name);

const fuelNewInstance = new Fuel({
const fuelNewInstance = await new Fuel({
connectors: [walletConnector, thirdPartyConnector],
storage,
});
}).init();
await fuelNewInstance.hasConnector();
expect(fuelNewInstance.currentConnector()?.name).toBe(thirdPartyConnector.name);
});
Expand All @@ -73,10 +73,10 @@ describe('Fuel Connector on browser', () => {
} as unknown as StorageAbstract;

const connector = new MockConnector();
const fuel = new Fuel({
const fuel = await new Fuel({
connectors: [connector],
storage,
});
}).init();

await fuel.hasConnector();
expect(storage.getItem).toBeCalledTimes(1);
Expand All @@ -93,9 +93,9 @@ describe('Fuel Connector on browser', () => {

it('should store on localStorage and remove on clean', async () => {
const connector = new MockConnector();
const fuel = new Fuel({
const fuel = await new Fuel({
connectors: [connector],
});
}).init();

await fuel.hasConnector();
const value = window.localStorage.getItem(Fuel.STORAGE_KEY);
Expand All @@ -108,9 +108,9 @@ describe('Fuel Connector on browser', () => {

it('should remove all listeners', async () => {
const connector = new MockConnector();
const fuel = new Fuel({
const fuel = await new Fuel({
connectors: [connector],
});
}).init();

await fuel.hasConnector();
const onConnection = vi.fn();
Expand All @@ -130,9 +130,9 @@ describe('Fuel Connector on browser', () => {

it('should remove all listeners and clean storage on destroy', async () => {
const connector = new MockConnector();
const fuel = new Fuel({
const fuel = await new Fuel({
connectors: [connector],
});
}).init();

await fuel.hasConnector();
const onConnection = vi.fn();
Expand Down
Loading
Loading