1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-26 09:33:22 +00:00

wire next achievement service to notifier service

This commit is contained in:
✨ Audrey ✨
2025-03-20 18:51:14 -04:00
parent d9edf1149c
commit 62339142a0
6 changed files with 134 additions and 106 deletions

View File

@@ -82,8 +82,7 @@ export class AchievementHub {
earned$(): Observable<Map<AchievementId, AchievementEarnedEvent>> {
return this.achievementLog.pipe(
filter((e) => isEarnedEvent(e)),
map((e) => e as AchievementEarnedEvent),
filter(isEarnedEvent),
latestEarnedMetrics(),
tap((m) => this.log.debug(m, "earned achievements update")),
startWith(new Map<AchievementId, AchievementEarnedEvent>()),
@@ -92,8 +91,7 @@ export class AchievementHub {
metrics$(): Observable<Map<MetricId, AchievementProgressEvent>> {
return this.achievementLog.pipe(
filter((e) => isProgressEvent(e)),
map((e) => e as AchievementProgressEvent),
filter(isProgressEvent),
latestProgressMetrics(),
tap((m) => this.log.debug(m, "achievement metrics update")),
startWith(new Map<MetricId, AchievementProgressEvent>()),

View File

@@ -1,4 +1,4 @@
import { Observable, OperatorFunction, map, pipe, withLatestFrom } from "rxjs";
import { Observable, OperatorFunction, combineLatestWith, map, pipe, withLatestFrom } from "rxjs";
import { AchievementId, AchievementValidator, MetricId } from "./types";
@@ -7,12 +7,15 @@ import { AchievementId, AchievementValidator, MetricId } from "./types";
function active(
metrics$: Observable<ReadonlyMap<MetricId, number>>,
earned$: Observable<ReadonlySet<AchievementId>>,
// TODO: accept a configuration observable that completes without
// emission when the user has opted out of achievements
): OperatorFunction<AchievementValidator[], AchievementValidator[]> {
return pipe(
// TODO: accept a configuration observable that completes without
// emission when the user has opted out of achievements
withLatestFrom(metrics$, earned$),
map(([monitors, metrics, earned]) => {
// refresh when an achievement is earned, but not when metrics
// update; this may cause metrics to overrun
withLatestFrom(metrics$),
combineLatestWith(earned$),
map(([[monitors, metrics], earned]) => {
// compute list of active achievements
const active = monitors.filter((m) => {
// 🧠 the filters could be lifted into a function argument & delivered

View File

@@ -1,52 +0,0 @@
import { filter, find, from, map, Observable } from "rxjs";
import { UserId } from "@bitwarden/common/types/guid";
import { AchievementHub } from "./achievement-hub";
import { AchievementService as AchievementServiceAbstraction } from "./achievement.service.abstraction";
import { EventStoreAbstraction } from "./event-store.abstraction.service";
import {
VaultItems_1_Added_Achievement,
VaultItems_10_Added_Achievement,
} from "./examples/achievements";
import { isEarnedEvent, isProgressEvent } from "./meta";
import {
Achievement,
AchievementEarnedEvent,
AchievementEvent,
AchievementId,
AchievementProgressEvent,
} from "./types";
// Service might be deprecated in favor of the AchievmentHub
// The hub is currently missing a way of listing all achievements, finding by id, but that could be possibly done via the AchievementManager
export class HubAchievementService implements AchievementServiceAbstraction {
private _achievements: Achievement[] = [
VaultItems_1_Added_Achievement,
VaultItems_10_Added_Achievement,
];
private _achievementsSubject = from(this._achievements);
earned$: Observable<AchievementEarnedEvent>;
inProgress$: Observable<AchievementProgressEvent>;
achievementById$: (achievementId: string) => Observable<Achievement>;
achievementsEarned$ = (userId: UserId) => { return this.earned$ };
achievementsInProgress$ = (userId: UserId) => { return this.inProgress$ }
private achievementHub = new AchievementHub();
constructor() {
this.achievementById$ = (achievementId: AchievementId) =>
this._achievementsSubject.pipe(find((item: Achievement) => item.name === achievementId));
this.earned$ = this.achievementHub.new$().pipe(filter((event) => isEarnedEvent(event)), map((event) => {
return event as AchievementEarnedEvent;
}));
this.inProgress$ = this.achievementHub.new$().pipe(filter((event) => isProgressEvent(event)), map((event) => {
return event as AchievementProgressEvent;
}));
}
}

View File

@@ -0,0 +1,100 @@
import { BehaviorSubject, EMPTY, filter, find, from, Observable } from "rxjs";
import { UserId } from "@bitwarden/common/types/guid";
import { Account } from "../../auth/abstractions/account.service";
import { UserEventLogProvider } from "../log/logger";
import { AchievementHub } from "./achievement-hub";
import { AchievementService as AchievementServiceAbstraction } from "./achievement.service.abstraction";
import { isEarnedEvent, isProgressEvent } from "./meta";
import {
Achievement,
AchievementEarnedEvent,
AchievementEvent,
AchievementProgressEvent,
AchievementValidator,
} from "./types";
import { ItemCreatedCountConfig } from "./validators/config/item-created-count-config";
import { SendItemCreatedCountConfig } from "./validators/config/send-created-count-config";
import { SendItemCreatedCountValidator } from "./validators/send-item-created-count-validator";
import { VaultItemCreatedCountValidator } from "./validators/vault-item-created-count-validator";
export class NextAchievementService implements AchievementServiceAbstraction {
constructor(private readonly eventLogs: UserEventLogProvider) {}
private hubs = new Map<string, AchievementHub>();
private getHub(account: Account) {
if (!this.hubs.has(account.id)) {
// FIXME: sync these from the server and load them
const validators$ = new BehaviorSubject<AchievementValidator[]>([
...VaultItemCreatedCountValidator.createValidators(ItemCreatedCountConfig.AllConfigs),
...SendItemCreatedCountValidator.createValidators(SendItemCreatedCountConfig.AllConfigs),
]);
// FIXME: load stored achievements
const achievements$ = from([] as AchievementEvent[]);
const events$ = this.eventLogs.monitor$(account);
const hub = new AchievementHub(validators$, events$, achievements$);
this.hubs.set(account.id, hub);
}
return this.hubs.get(account.id)!;
}
private _achievements: Achievement[] = [
...ItemCreatedCountConfig.AllConfigs,
...SendItemCreatedCountConfig.AllConfigs,
];
private _achievementsSubject = from(this._achievements);
achievementMap() {
return new Map(this._achievements.map((a) => [a.achievement, a] as const));
}
earnedStream$(account: Account, all: boolean = false) {
const hub = this.getHub(account);
if (all) {
return hub.all$().pipe(filter(isEarnedEvent));
} else {
return hub.new$().pipe(filter(isEarnedEvent));
}
}
earnedMap$(account: Account) {
return this.getHub(account).metrics$();
}
progressStream$(account: Account, all: boolean = false) {
const hub = this.getHub(account);
if (all) {
return hub.all$().pipe(filter(isProgressEvent));
} else {
return hub.new$().pipe(filter(isProgressEvent));
}
}
metricsMap$(account: Account) {
return this.getHub(account).metrics$();
}
achievementById$(achievementId: string): Observable<Achievement> {
return this._achievementsSubject.pipe(
find((item: Achievement) => item.name === achievementId),
filter((f): f is Achievement => !!f),
);
}
earned$: Observable<AchievementEarnedEvent> = EMPTY;
inProgress$: Observable<AchievementProgressEvent> = EMPTY;
achievementsEarned$(userId: UserId): Observable<AchievementEarnedEvent> {
return EMPTY;
}
achievementsInProgress$(userId: UserId): Observable<AchievementProgressEvent> {
return EMPTY;
}
}

View File

@@ -1,4 +1,4 @@
import { BehaviorSubject, SubjectLike, from, map, zip } from "rxjs";
import { BehaviorSubject, Observable, SubjectLike, from, map, zip } from "rxjs";
import { Primitive } from "type-fest";
import { Account } from "../../auth/abstractions/account.service";
@@ -11,7 +11,8 @@ import { disabledSemanticLoggerProvider } from "./factory";
import { SemanticLogger } from "./semantic-logger.abstraction";
export abstract class UserEventLogProvider {
abstract create: (account: Account) => UserEventLogger;
abstract capture: (account: Account) => UserEventLogger;
abstract monitor$: (account: Account) => Observable<UserActionEvent>;
}
type BaselineType = Omit<ServiceFormat & UserFormat, "@timestamp">;