Skip to content

feat: Add subscribeOnce and waitUntil methods#8575

Draft
FrederikBolding wants to merge 7 commits intomainfrom
fb/add-subscribeOnce-and-waitUntil
Draft

feat: Add subscribeOnce and waitUntil methods#8575
FrederikBolding wants to merge 7 commits intomainfrom
fb/add-subscribeOnce-and-waitUntil

Conversation

@FrederikBolding
Copy link
Copy Markdown
Member

@FrederikBolding FrederikBolding commented Apr 24, 2026

Explanation

Add subscribeOnce and waitUntil utility methods to Messenger. These are useful for subscribing to and waiting for a certain event to fire without wanting to listen for more than one invocation.

Additionally the methods support a condition parameter which can be used as a predicate for whether the event should be consumed in the first place.

A similar approach is already in-use on mobile, this is a "native" replacement.

References

https://consensyssoftware.atlassian.net/browse/WPC-985

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

const promise = messenger.waitUntil('Fixture:message');
messenger.publish('Fixture:message', 'foo');

expect(await promise).toBe('foo');
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

When not using a selector this just returns the first parameter of the event. The other option is returning the entire array, but that feels a bit complicated 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, why does it feel complicated? If it's easy to support, I say we give consumers the whole payload.

Comment thread packages/messenger/src/Messenger.ts Outdated
Comment on lines +759 to +760
selector: SelectorFunction<Event, EventType, SelectorReturnValue>,
condition?: (value: SelectorReturnValue) => boolean,
Copy link
Copy Markdown
Member

@Mrtenz Mrtenz Apr 24, 2026

Choose a reason for hiding this comment

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

What if you want to specify a condition, but not a selector? Like this for example:

// Subscribe to `KeyringController:unlock` and wait if condition is not met
await messenger.waitUntil(
  'KeyringController:unlock',
  () => !engineContext.KeyringController.isUnlocked()
);

Maybe it's easier to have an options object, so you could do something like this:

await messenger.waitUntil(
  'KeyringController:unlock',
  {
    condition: () => !engineContext.KeyringController.isUnlocked()
  }
);

Copy link
Copy Markdown
Member Author

@FrederikBolding FrederikBolding Apr 24, 2026

Choose a reason for hiding this comment

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

I'm confused by this question, if KeyringController.isUnlocked is true you wouldn't want to wait for unlock in the first place. The condition is meant as a condition on the value itself and is only checked when an event fires, e.g.

await messenger.waitUntil(
  'TransactionController:transactionSuccess',
  (tx) => tx.hash === 'foo'
);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That being said, it would be nice if you could pass a condition without a selector. Potentially worth doing an options bag regardless.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Then I was misunderstanding what the condition is supposed to do 😅

Copy link
Copy Markdown
Member

@Mrtenz Mrtenz Apr 24, 2026

Choose a reason for hiding this comment

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

I was under the impression that these would be equivalent:

await messenger.waitUntil(
  'KeyringController:unlock',
  {
    condition: () => !engineContext.KeyringController.isUnlocked()
  }
);

and

if (!engineContext.KeyringController.isUnlocked()) {
  await messenger.waitUntil('KeyringController:unlock');
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't see an obvious way to solve for both that and the TransactionController example from above 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, we'd need to introduce a third property. But in hindsight, I'm not sure if it's necessary as the if statement seems clean enough.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Certainly better than what we have today

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

| ((value: SelectorReturnValue) => boolean);
},
): Promise<SelectorReturnValue | ExtractEventPayload<Event, EventType>[0]> {
return new Promise((resolve) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How we expect waitUntil to be used? In Slack you gave this example:

  getUnlockPromise: () => {
    if (engineContext.KeyringController.isUnlocked()) {
      return Promise.resolve();
    }
    return new Promise<void>((resolve) => {
      controllerMessenger.subscribeOnceIf(
        'KeyringController:unlock',
        resolve,
        () => true,
      );
    });
  },

Is the idea that with this method you could shorten this to the following?

  getUnlockPromise: () => {
    return await controllerMessenger.waitUntil(
      'KeyringController:unlock',
      {
        condition: () => {
          return engineContext.KeyringController.isUnlocked();
        }
      }
    );
  },

Or is it that we would expect people to write this instead?

  getUnlockPromise: () => {
    return await controllerMessenger.waitUntil(
      'KeyringController:stateChange',
      {
        selector: (state) => state.isUnlocked,
      }
    );
  },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Either way would we want to check the condition beforehand to make sure is not true before subscribing? (I guess the same goes for subscribeOnce as well.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants