mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 05:30:01 +00:00
factor measurement and achievement earning into separate phases
This commit is contained in:
@@ -10,6 +10,12 @@ import {
|
||||
import { isEarnedEvent, isProgressEvent } from "./meta";
|
||||
import { AchievementEvent, AchievementValidator } from "./types";
|
||||
|
||||
function mapProgressByName(status: AchievementEvent[]) {
|
||||
return new Map(
|
||||
status.filter(isProgressEvent).map((e) => [e.achievement.name, e.achievement.value] as const),
|
||||
);
|
||||
}
|
||||
|
||||
// OPTIMIZATION: compute the list of active monitors from trigger criteria
|
||||
function active(
|
||||
status$: Observable<AchievementEvent[]>,
|
||||
@@ -18,9 +24,7 @@ function active(
|
||||
withLatestFrom(status$),
|
||||
map(([monitors, log]) => {
|
||||
// partition the log into progress and earned achievements
|
||||
const progressByName = new Map(
|
||||
log.filter(isProgressEvent).map((e) => [e.achievement.name, e.achievement.value]),
|
||||
);
|
||||
const progressByName = mapProgressByName(log);
|
||||
const earnedByName = new Set(
|
||||
log.filter((e) => isEarnedEvent(e)).map((e) => e.achievement.name),
|
||||
);
|
||||
@@ -36,7 +40,7 @@ function active(
|
||||
}
|
||||
|
||||
// monitor disabled if outside of threshold
|
||||
const progress = progressByName.get(m.progress) ?? 0;
|
||||
const progress = (m.metric && progressByName.get(m.metric)) || 0;
|
||||
if (progress > (m.trigger.high ?? Number.POSITIVE_INFINITY)) {
|
||||
return false;
|
||||
} else if (progress < (m.trigger.low ?? 0)) {
|
||||
@@ -70,10 +74,17 @@ function validate(
|
||||
|
||||
// process achievement monitors sequentially, accumulating result records
|
||||
for (const monitor of monitors) {
|
||||
const statusEntry = status.find((s) => s.achievement.name === monitor.achievement);
|
||||
const progress = isProgressEvent(statusEntry) ? statusEntry : undefined;
|
||||
const progress = mapProgressByName(status);
|
||||
|
||||
results.push(...monitor.action(action, progress));
|
||||
const measurements = monitor.measure(action, progress);
|
||||
results.push(...measurements);
|
||||
|
||||
// modify copy produced by filter to avoid reallocation
|
||||
for (const m of measurements) {
|
||||
progress.set(m.achievement.name, m.achievement.value);
|
||||
}
|
||||
|
||||
results.push(...monitor.earn(progress));
|
||||
}
|
||||
|
||||
// deliver results as a stream containing individual records to maintain
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { UserId } from "../../types/guid";
|
||||
|
||||
import {
|
||||
AchievementEarnedEvent,
|
||||
AchievementId,
|
||||
AchievementProgressEvent,
|
||||
AchievementProgressId,
|
||||
} from "./types";
|
||||
import { AchievementEarnedEvent, AchievementId, AchievementProgressEvent, MetricId } from "./types";
|
||||
|
||||
export function progressEvent(
|
||||
name: AchievementProgressId,
|
||||
value: number = 1,
|
||||
): AchievementProgressEvent {
|
||||
export function progressEvent(name: MetricId, value: number = 1): AchievementProgressEvent {
|
||||
return {
|
||||
"@timestamp": Date.now(),
|
||||
event: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Type } from "./data";
|
||||
import { earnedEvent, progressEvent } from "./events";
|
||||
import { AchievementId, AchievementProgressId, AchievementValidator } from "./types";
|
||||
import { AchievementId, MetricId, AchievementValidator } from "./types";
|
||||
|
||||
const ItemCreatedProgress = "item-created-progress" as AchievementProgressId;
|
||||
const ItemCreatedProgress = "item-quantity" as MetricId;
|
||||
|
||||
const ItemCreatedAchievement = "item-created" as AchievementId;
|
||||
const ThreeItemsCreatedAchievement = "three-vault-items-created" as AchievementId;
|
||||
@@ -10,58 +10,57 @@ const FiveItemsCreatedAchievement = "five-vault-items-created" as AchievementId;
|
||||
|
||||
const ItemCreatedValidator = {
|
||||
achievement: ItemCreatedAchievement,
|
||||
progress: ItemCreatedProgress,
|
||||
metric: ItemCreatedProgress,
|
||||
evaluator: Type.Threshold,
|
||||
trigger: "once",
|
||||
hidden: false,
|
||||
filter(item) {
|
||||
return item.action === "vault-item-added";
|
||||
},
|
||||
action(item, progress) {
|
||||
return [progressEvent(ItemCreatedProgress), earnedEvent(ItemCreatedAchievement)];
|
||||
measure(item, progress) {
|
||||
return [progressEvent(ItemCreatedProgress)];
|
||||
},
|
||||
earn(progress) {
|
||||
return [earnedEvent(ItemCreatedAchievement)];
|
||||
},
|
||||
} satisfies AchievementValidator;
|
||||
|
||||
const ThreeItemsCreatedValidator = {
|
||||
achievement: ThreeItemsCreatedAchievement,
|
||||
progress: ItemCreatedProgress,
|
||||
metric: ItemCreatedProgress,
|
||||
evaluator: Type.Threshold,
|
||||
trigger: { low: 2, high: 3 },
|
||||
hidden: false,
|
||||
filter(item) {
|
||||
return item.action === "vault-item-added";
|
||||
},
|
||||
action(item, progress) {
|
||||
if (!progress) {
|
||||
return [progressEvent(ItemCreatedProgress)];
|
||||
}
|
||||
|
||||
const value = progress.achievement.value + 1;
|
||||
if (value >= 3) {
|
||||
return [earnedEvent(ThreeItemsCreatedAchievement)];
|
||||
}
|
||||
|
||||
measure(_item, progress) {
|
||||
const value = 1 + (progress.get(ItemCreatedProgress) ?? 0);
|
||||
return [progressEvent(ItemCreatedProgress, value)];
|
||||
},
|
||||
earn(progress) {
|
||||
const value = progress.get(ItemCreatedProgress) ?? 0;
|
||||
return value >= 3 ? [earnedEvent(ItemCreatedAchievement)] : [];
|
||||
},
|
||||
} satisfies AchievementValidator;
|
||||
|
||||
const FiveItemsCreatedValidator = {
|
||||
achievement: ThreeItemsCreatedAchievement,
|
||||
progress: ItemCreatedProgress,
|
||||
metric: ItemCreatedProgress,
|
||||
evaluator: Type.Threshold,
|
||||
trigger: { low: 4, high: 5 },
|
||||
hidden: false,
|
||||
filter(item) {
|
||||
return item.action === "vault-item-added";
|
||||
},
|
||||
action(item, progress) {
|
||||
const value = 1 + (progress?.achievement?.value ?? 0);
|
||||
if (value >= 3) {
|
||||
return [earnedEvent(FiveItemsCreatedAchievement)];
|
||||
}
|
||||
|
||||
measure(_item, progress) {
|
||||
const value = 1 + (progress.get(ItemCreatedProgress) ?? 0);
|
||||
return [progressEvent(ItemCreatedProgress, value)];
|
||||
},
|
||||
earn(progress) {
|
||||
const value = progress.get(ItemCreatedProgress) ?? 0;
|
||||
return value >= 5 ? [earnedEvent(ItemCreatedAchievement)] : [];
|
||||
},
|
||||
} satisfies AchievementValidator;
|
||||
|
||||
export {
|
||||
|
||||
@@ -7,11 +7,11 @@ import { Type } from "./data";
|
||||
|
||||
export type EvaluatorType = keyof typeof Type;
|
||||
export type AchievementId = string & Tagged<"achievement">;
|
||||
export type AchievementProgressId = string & Tagged<"achievement-progress">;
|
||||
export type MetricId = string & Tagged<"metric-id">;
|
||||
|
||||
export type AchievementProgressEvent = EventFormat &
|
||||
ServiceFormat &
|
||||
UserFormat & { achievement: { type: "progress"; name: AchievementProgressId; value: number } };
|
||||
UserFormat & { achievement: { type: "progress"; name: MetricId; value: number } };
|
||||
export type AchievementEarnedEvent = EventFormat &
|
||||
ServiceFormat &
|
||||
UserFormat & { achievement: { type: "earned"; name: AchievementId } };
|
||||
@@ -20,7 +20,9 @@ export type AchievementEvent = AchievementProgressEvent | AchievementEarnedEvent
|
||||
// consumed by validator and achievement list (should this include a "toast-alerter"?)
|
||||
export type Achievement = {
|
||||
achievement: AchievementId;
|
||||
progress: AchievementProgressId;
|
||||
|
||||
metric?: MetricId;
|
||||
|
||||
evaluator: EvaluatorType;
|
||||
|
||||
// pre-filter that disables the rule if it's met
|
||||
@@ -34,9 +36,9 @@ export type AchievementValidator = Achievement & {
|
||||
// when the watch triggers on incoming user events
|
||||
filter: (item: EventFormat) => boolean;
|
||||
|
||||
// what to do when an incoming event is triggered
|
||||
action: (
|
||||
item: EventFormat,
|
||||
progress?: AchievementProgressEvent,
|
||||
) => [AchievementEvent] | [AchievementEvent, AchievementEvent];
|
||||
// observe data from the event stream and produces measurements
|
||||
measure: (item: EventFormat, progress: Map<MetricId, number>) => AchievementProgressEvent[];
|
||||
|
||||
// monitors achievement progress and emits earned achievements
|
||||
earn: (progress: Map<MetricId, number>) => AchievementEarnedEvent[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user