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:
@@ -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 };
|
||||
|
||||
@@ -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)] : [];
|
||||
},
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user