Skip to content

feat: Add subscribeOnce and waitUntil methods#8575

Open
FrederikBolding wants to merge 9 commits intomainfrom
fb/add-subscribeOnce-and-waitUntil
Open

feat: Add subscribeOnce and waitUntil methods#8575
FrederikBolding wants to merge 9 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

Note

Medium Risk
Adds new subscription/promise utilities on the core Messenger event path, which could introduce subtle behavior or typing issues around selectors, conditions, and auto-unsubscribe semantics.

Overview
Adds Messenger.subscribeOnce to subscribe to an event (optionally with selector + predicate) and automatically unsubscribe after the first matching invocation.

Adds Messenger.waitUntil, a Promise-based helper built on subscribeOnce, resolving with the next matching event payload/selected value.

Updates tests to cover one-shot subscription behavior, selector support, and conditional consumption, and documents the new APIs in the package changelog.

Reviewed by Cursor Bugbot for commit eb007dc. Bugbot is set up for automated code reviews on this repo. Configure here.

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.

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.

Ah, I should have clarified. What I was worried about is the ergonomics of dealing with the entire payload array versus just the first parameter (since more often than not the first parameter is probably what you want). But it may be fine.

See 0ab75e3

Comment thread packages/messenger/src/Messenger.ts Outdated
| ((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.)

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.

The condition is only applied when a state update comes in, it is meant for a scenario like this:

await messenger.waitUntil(
  'TransactionController:transactionSuccess',
  { condition: (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.

For the unlock example it would likely have to be:

  getUnlockPromise: () => {
    if (engineContext.KeyringController.isUnlocked()) {
      return Promise.resolve();
    }
    return controllerMessenger.waitUntil('KeyringController:unlock');
  },

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.

Gotcha. If we mainly would use condition for waiting for an object to reach a certain status, then do we need condition at all? I feel like having a specialized event solves the same problem but without needing to check anything else at runtime:

await messenger.waitFor(
  `TransactionController:transactionSuccess:${tx.hash}`,
);

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.

Sure, that's another way of doing it. But I was under the impression that we agreed on having both tools would be useful and that forcing more granular events is not necessarily the path we wanted.

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.

The value you want to check for may also not necessarily be serialisable to a string.

@FrederikBolding FrederikBolding marked this pull request as ready for review April 27, 2026 11:00
@FrederikBolding FrederikBolding requested a review from a team as a code owner April 27, 2026 11:00
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0ab75e3. Configure here.

Comment thread packages/messenger/src/Messenger.ts
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