1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

improve validator flexibility; add documentation

This commit is contained in:
✨ Audrey ✨
2025-03-12 17:29:26 -04:00
parent a33403be86
commit 69f1c65ea5
3 changed files with 35 additions and 48 deletions

View File

@@ -1,11 +1,5 @@
import { Observable, OperatorFunction, concatMap, from, map, pipe, withLatestFrom } from "rxjs";
import { active } from "./achievement-manager";
import {
achievementMonitors$,
achievementsLocal$ as achievementsLog$,
userActionIn$,
} from "./inputs";
import {
AchievementEvent,
AchievementId,
@@ -14,68 +8,67 @@ import {
MetricId,
UserActionEvent,
} from "./types";
import { mapProgressByName } from "./util";
import { mapProgressByName as toMetricMap } from "./util";
// the formal event processor
function validate(
/** Monitors a user activity stream to recognize achievements
* @param validators$ validators track achievement progress and award achievements
* @param captured$ the set of previously emitted achievement events
*/
function achievements(
validators$: Observable<AchievementValidator[]>,
captured$: Observable<AchievementEvent[]>,
): OperatorFunction<UserActionEvent, AchievementEvent> {
return pipe(
withLatestFrom(validators$),
// narrow the list of all live monitors to just those that may produce new logs
map(([action, monitors]) => {
// narrow the list of all live monitors to just those that may produce new logs
const triggered = monitors.filter((m) => m.filter(action));
return [action, triggered] as const;
}),
withLatestFrom(captured$),
// monitor achievements
concatMap(([[action, validators], captured]) => {
const results: AchievementEvent[] = [];
const progress = mapProgressByName(captured);
const measurements = new Map<AchievementId, AchievementProgressEvent[]>();
const achievements: AchievementEvent[] = [];
const metrics = toMetricMap(captured);
const progress = new Map<AchievementId, AchievementProgressEvent[]>();
// collect measurements
for (const validator of validators) {
const measured = validator.measure(action, progress);
measurements.set(validator.achievement, measured);
results.push(...measured);
const measured = validator.measure?.(action, metrics) ?? [];
progress.set(validator.achievement, measured);
achievements.push(...measured);
}
// update processor's internal progress values
const distinct = new Map<MetricId, AchievementId>();
const entries = [...measurements.entries()].flatMap(([a, ms]) =>
ms.map((m) => [a, m] as const),
);
const entries = [...progress.entries()].flatMap(([a, ms]) => ms.map((m) => [a, m] as const));
for (const [achievement, measured] of entries) {
const key = measured.achievement.name;
// prevent duplicate updates
if (distinct.has(key)) {
const msg = `${achievement} failed to set set ${key} value already set by ${distinct.get(key)}`;
const msg = `${achievement} failed to set ${key}; value already set by ${distinct.get(key)}`;
throw new Error(msg);
}
distinct.set(key, achievement);
progress.set(measured.achievement.name, measured.achievement.value);
metrics.set(measured.achievement.name, measured.achievement.value);
}
// detect earned achievements
for (const validator of validators) {
const measured = measurements.get(validator.achievement) ?? [];
const earned = validator.earn(measured, progress);
results.push(...earned);
const events = progress.get(validator.achievement) ?? [];
const awarded = validator.award?.(events, metrics) ?? [];
achievements.push(...awarded);
}
// deliver results as a stream containing individual records to maintain
// the map/reduce model of the validator
return from(results);
return from(achievements);
}),
);
}
// monitors are lazy until their trigger condition is met
const liveMonitors$ = achievementMonitors$.pipe(active(achievementsLog$));
// pre-wired achievement stream; this is the prototype's host, and
// in the full version is wired by the application
const validatedAchievements$ = userActionIn$.pipe(validate(liveMonitors$, achievementsLog$));
export { validate, validatedAchievements$ };
export { achievements as validate };

View File

@@ -20,10 +20,7 @@ const TotallyAttachedValidator = {
filter(item) {
return item.tags?.includes("with-attachment") ?? false;
},
measure(item, progress) {
return [];
},
earn(progress) {
award(progress) {
return [earnedEvent(TotallyAttachedAchievement)];
},
} satisfies AchievementValidator;
@@ -49,9 +46,6 @@ const ItemCreatedTracker = {
const value = 1 + (progress.get(ItemCreatedProgress) ?? 0);
return [progressEvent(ItemCreatedProgress, value)];
},
earn(progress) {
return [];
},
} satisfies AchievementValidator;
const ItemCreatedValidator = {
@@ -67,7 +61,7 @@ const ItemCreatedValidator = {
measure(item, progress) {
return [progressEvent(ItemCreatedProgress)];
},
earn(progress) {
award(progress) {
return [earnedEvent(ItemCreatedAchievement)];
},
} satisfies AchievementValidator;
@@ -86,7 +80,7 @@ const ThreeItemsCreatedValidator = {
const value = 1 + (progress.get(ItemCreatedProgress) ?? 0);
return [progressEvent(ItemCreatedProgress, value)];
},
earn(_measured, progress) {
award(_measured, progress) {
const value = progress.get(ItemCreatedProgress) ?? 0;
return value >= 3 ? [earnedEvent(ItemCreatedAchievement)] : [];
},
@@ -106,7 +100,7 @@ const FiveItemsCreatedValidator = {
const value = 1 + (progress.get(ItemCreatedProgress) ?? 0);
return [progressEvent(ItemCreatedProgress, value)];
},
earn(_measured, progress) {
award(_measured, progress) {
const value = progress.get(ItemCreatedProgress) ?? 0;
return value >= 5 ? [earnedEvent(ItemCreatedAchievement)] : [];
},

View File

@@ -47,11 +47,11 @@ export type AchievementValidator = Achievement & {
filter: (item: EventFormat) => boolean;
// observe data from the event stream and produces measurements
measure: (item: EventFormat, progress: Map<MetricId, number>) => AchievementProgressEvent[];
measure?: (item: EventFormat, metrics: Map<MetricId, number>) => AchievementProgressEvent[];
// monitors achievement progress and emits earned achievements
earn: (
measured: AchievementProgressEvent[],
progress: Map<MetricId, number>,
award?: (
events: AchievementProgressEvent[],
metrics: Map<MetricId, number>,
) => AchievementEarnedEvent[];
};