-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Add Web Push Support #11346
base: main
Are you sure you want to change the base?
Add Web Push Support #11346
Conversation
This comment was marked as off-topic.
This comment was marked as off-topic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this might need a lot more documentation. I'm not understanding what this is for from the code and tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added some more documentation to the method but here is a snippet from it:
interface MyService {
event$(userId: UserId) => Observable<UserEvent>
}
// Test
const myService = mock<MyService>();
const eventGetter = Matrix.autoMockMethod(myService.event$, (userId) => BehaviorSubject<UserEvent>());
eventGetter("userOne").next(new UserEvent());
eventGetter("userTwo").next(new UserEvent());
This replaces a more manual way of doing things like:
const myService = mock<MyService>();
const userOneSubject = new BehaviorSubject<UserEvent>();
const userTwoSubject = new BehaviorSubject<UserEvent>();
myService.event$.mockImplementation((userId) => {
if (userId === "userOne") {
return userOneSubject;
} else if (userId === "userTwo") {
return userTwoSubject;
}
return new BehaviorSubject<UserEvent>();
});
userOneSubject.next(new UserEvent());
userTwoSubject.next(new UserEvent());
Basically so I can easily emit values for different users easily in tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, the problem it's solving is that the pattern we're using of parametrized observables is pretty wordy to set up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this barrel file really doing anything for you if it's
- already internal and
- exporting everything from every file in the folder?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah it makes importing internal things a lot easier. It's also supposed to automatically make it a restricted import outside of platform code but it appears that (all?) our restricted import eslint rules aren't working.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've wanted that behavior for barrel files, but I don't believe we've ever found or built a rule to enforce that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was working:
Lines 331 to 346 in 18f7d64
{ | |
"files": ["**/*.ts"], | |
"excludedFiles": ["**/platform/**/*.ts"], | |
"rules": { | |
"no-restricted-imports": [ | |
"error", | |
{ | |
"patterns": [ | |
"**/platform/**/internal", // General internal pattern | |
// All features that have been converted to barrel files | |
"**/platform/messaging/**" | |
] | |
} | |
] | |
} | |
}, |
I should just be able to add **/platform/notifications/**
but it's not working now and I don't want to add it now in case it spontaneously starts working again.
abstract startListening(): Subscription; | ||
abstract reconnectFromActivity(): void; | ||
abstract disconnectFromInactivity(): void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I'd love to change this interface to something that makes more sense.
really notifications connect or disconnect due to account switching, locking, etc.
What's more, there's not a big reason to disconnect notifications for non active (but logged in) users once we use web push
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do too, I wanted to yank them out alongside the updateConnection
method but didn't want to expand the scope that much.
I envision there would probably be a ActivityService
with activity$
and that could be part of the chain instead of it tracking it's own BehaviorSubject
. Then as you say, we could move it into just the SignalR code so that web push would be allowed to stay connected when there isn't activity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we already have tickets tracking that? Either way we should add some TODOs here to document the planned interface changes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added one & and comment. https://bitwarden.atlassian.net/browse/PM-14264
if (authStatus !== AuthenticationStatus.Unlocked) { | ||
return EMPTY; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we currently require unlocked? I think all the notifications could be done on a locked vault, and listening for logout events on locked is useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly, to do any sort of reconnect
or updateConnection
you do have to be authed and unlocked. But technically the very first connect does NOT enforce that. So if your application called init
while you were locked you can connect. I'm happy to change it because receiving logout events is very useful but I do believe there are some preliminary changes to make the access token be stored encrypted by the user key which would make it unattainable unless you are unlocked.
clients/libs/common/src/services/notifications.service.ts
Lines 223 to 251 in 073ee47
private async reconnect(sync: boolean) { | |
this.reconnectTimerSubscription?.unsubscribe(); | |
if (this.connected || !this.inited || this.inactive) { | |
return; | |
} | |
const authedAndUnlocked = await this.isAuthedAndUnlocked(); | |
if (!authedAndUnlocked) { | |
return; | |
} | |
try { | |
await this.signalrConnection.start(); | |
this.connected = true; | |
if (sync) { | |
await this.syncService.fullSync(false); | |
} | |
} catch (e) { | |
this.logService.error(e); | |
} | |
if (!this.connected) { | |
this.isSyncingOnReconnect = sync; | |
this.reconnectTimerSubscription = this.taskSchedulerService.setTimeout( | |
ScheduledTaskNames.notificationsReconnectTimeout, | |
this.random(120000, 300000), | |
); | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm. encrypting the access token with the user key effectively kills the locked state and would split it into two states:
- just authed (you just authed but haven't yet unlocked; so you have the auth token, but not the user key)
- maybe authed (you just locked; so you don't have the auth token or user key)
@JaredSnider-Bitwarden let's sync up about this when you're available. Don't want to derail here too badly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
concatMap((subscription) => { | ||
if (subscription == null) { | ||
return this.pushManagerSubscribe$(key); | ||
} | ||
|
||
const subscriptionKey = Utils.fromBufferToUrlB64( | ||
subscription.options?.applicationServerKey, | ||
); | ||
if (subscriptionKey !== key) { | ||
// There is a subscription, but it's not for the current server, unsubscribe and then make a new one | ||
return defer(() => subscription.unsubscribe()).pipe( | ||
concatMap(() => this.pushManagerSubscribe$(key)), | ||
); | ||
} | ||
|
||
return of(subscription); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: yikes. this is rough logic. I would be significantly improved if we did one of
- just allow multiple subscriptions to servers and filter some other way -- I believe all notifications include tag information
- extract some of this logic into methods as you did with
pushManagerSubscribe$
- not be so dedicated to all of these being observables at every level
I personally like 1 the most, but it's also probably the most work. At the least you'd want to carefully unsubscribe a user on logout
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went with 3 for now, I can simplify it a whole lot by just doing all of it in a single defer
, it does make it much simpler. Regarding 1 though, I think this will already reuse the same subscription. I intentionally left out any eager unsubscribe
and non-active user notifications are filtered out in notifications service.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure where I was coming from with 1, I'm pretty sure I tried to create multiple subscriptions and you can't. Worth double checking, but you're probably right we need to update existing subscriptions, which means rethinking our tags.
libs/common/src/platform/notifications/internal/signalr-connection.service.ts
Outdated
Show resolved
Hide resolved
libs/common/src/platform/notifications/internal/signalr-connection.service.ts
Outdated
Show resolved
Hide resolved
libs/common/src/platform/notifications/internal/signalr-connection.service.ts
Outdated
Show resolved
Hide resolved
libs/common/src/platform/notifications/internal/signalr-connection.service.ts
Outdated
Show resolved
Hide resolved
Co-authored-by: Matt Gibson <[email protected]>
…-connection.service.ts Co-authored-by: Matt Gibson <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything addressed 👍
libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts
Outdated
Show resolved
Hide resolved
emitActiveUser(mockUser1); | ||
emitNotificationUrl(DISABLED_NOTIFICATIONS_URL); | ||
|
||
expect(signalRNotificationConnectionService.connect$).not.toHaveBeenCalled(); | ||
expect(webPushNotificationConnectionService.supportStatus$).not.toHaveBeenCalled(); | ||
|
||
subscription.unsubscribe(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: If we get issues with people making changes to this file and forgets these unsubscribes we could look into cleaning in afterEach
or beforeEach
libs/common/src/platform/notifications/internal/default-notifications.service.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few tiny questions (per discussion, I'm not actually a required reviewer):
@@ -17,6 +23,7 @@ export class ServerConfig { | |||
environment?: EnvironmentServerConfigData; | |||
utcDate: Date; | |||
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; | |||
push: PushConfig; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit (non-blocking): pushConfig
is a more clear name vs just push
.
🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-9609
📔 Objective
Main objective is adding
WebPush
support to our MV3 browser clients but it does add the ability to relatively easily add support for it in other clients. We only attempt to use web push if we call out to the server and the server says that it supports it, effectively being a feature flag.Part of this, notifications goes more observable where it will listen to the observables it cares about, making it not need it's
updateConnection
method anymore.📸 Screenshots
⏰ Reminders before review
🦮 Reviewer guidelines
:+1:
) or similar for great changes:memo:
) or ℹ️ (:information_source:
) for notes or general info:question:
) for questions:thinking:
) or 💭 (:thought_balloon:
) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion:art:
) for suggestions / improvements:x:
) or:warning:
) for more significant problems or concerns needing attention:seedling:
) or ♻️ (:recycle:
) for future improvements or indications of technical debt:pick:
) for minor or nitpick changes