mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 05:30:01 +00:00
tweaks from setting up models
This commit is contained in:
8
libs/common/src/tools/achievements/data.ts
Normal file
8
libs/common/src/tools/achievements/data.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const Type = Object.freeze({
|
||||
TimeOfDayRange: "TimeOfDayRange",
|
||||
DayOfWeek: "DayOfWeek",
|
||||
DayOfYear: "DayOfYear",
|
||||
Threshold: "Threshold",
|
||||
});
|
||||
|
||||
export const EvaluatorTypes = Object.freeze(Object.keys(Type) as ReadonlyArray<keyof typeof Type>);
|
||||
@@ -7,42 +7,39 @@ import {
|
||||
achievementsLocal$ as achievementsLog$,
|
||||
userActionIn$,
|
||||
} from "./inputs";
|
||||
import { isProgress } from "./meta";
|
||||
import { AchievementFormat, AchievementWatch, Earned, Progress } from "./types";
|
||||
import { isEarnedEvent, isProgressEvent } from "./meta";
|
||||
import { AchievementEvent, AchievementValidator } from "./types";
|
||||
|
||||
// OPTIMIZATION: compute the list of active monitors from trigger criteria
|
||||
function active(
|
||||
status$: Observable<AchievementFormat[]>,
|
||||
): OperatorFunction<AchievementWatch[], AchievementWatch[]> {
|
||||
status$: Observable<AchievementEvent[]>,
|
||||
): OperatorFunction<AchievementValidator[], AchievementValidator[]> {
|
||||
return pipe(
|
||||
withLatestFrom(status$),
|
||||
map(([monitors, log]) => {
|
||||
// partition the log into progress and earned achievements
|
||||
const progress: Progress[] = [];
|
||||
const earned: Earned[] = [];
|
||||
for (const l of log) {
|
||||
if (isProgress(l.achievement)) {
|
||||
progress.push(l.achievement);
|
||||
} else {
|
||||
earned.push(l.achievement);
|
||||
}
|
||||
}
|
||||
|
||||
const progressByName = new Map(progress.map((a) => [a.name, a.value]));
|
||||
const earnedByName = new Set(earned.map((e) => e.name));
|
||||
const progressByName = new Map(
|
||||
log.filter(isProgressEvent).map((e) => [e.achievement.name, e.achievement.value]),
|
||||
);
|
||||
const earnedByName = new Set(
|
||||
log.filter((e) => isEarnedEvent(e)).map((e) => e.achievement.name),
|
||||
);
|
||||
|
||||
// compute list of active achievements
|
||||
const active = monitors.filter((m) => {
|
||||
// 🧠 the filters could be lifted into a function argument & delivered
|
||||
// as a `Map<FilterType, (monitor) => bool>
|
||||
|
||||
if (m.trigger === "once") {
|
||||
// monitor disabled if already achieved
|
||||
return !earnedByName.has(m.achievement);
|
||||
}
|
||||
|
||||
// monitor disabled if outside of threshold
|
||||
const progress = progressByName.get(m.achievement) ?? 0;
|
||||
if (m.trigger.high ?? Number.POSITIVE_INFINITY < progress) {
|
||||
const progress = progressByName.get(m.progress) ?? 0;
|
||||
if (progress > (m.trigger.high ?? Number.POSITIVE_INFINITY)) {
|
||||
return false;
|
||||
} else if (m.trigger.low ?? 0 > progress) {
|
||||
} else if (progress < (m.trigger.low ?? 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -57,27 +54,26 @@ function active(
|
||||
|
||||
// the formal event processor
|
||||
function validate(
|
||||
monitors$: Observable<AchievementWatch[]>,
|
||||
status$: Observable<AchievementFormat[]>,
|
||||
): OperatorFunction<EventFormat, AchievementFormat> {
|
||||
monitors$: Observable<AchievementValidator[]>,
|
||||
status$: Observable<AchievementEvent[]>,
|
||||
): OperatorFunction<EventFormat, AchievementEvent> {
|
||||
return pipe(
|
||||
withLatestFrom(monitors$),
|
||||
map(([action, monitors]) => {
|
||||
// narrow the list of all live monitors to just those that may
|
||||
// change the log
|
||||
// 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(status$),
|
||||
concatMap(([[action, monitors], status]) => {
|
||||
const results: AchievementFormat[] = [];
|
||||
const results: AchievementEvent[] = [];
|
||||
|
||||
// process achievement monitors sequentially, accumulating result records
|
||||
for (const monitor of monitors) {
|
||||
const statusEntry = status.find(
|
||||
(s) => s.achievement.name === monitor.achievement && isProgress(s.achievement),
|
||||
);
|
||||
results.push(...monitor.action(action, statusEntry));
|
||||
const statusEntry = status.find((s) => s.achievement.name === monitor.achievement);
|
||||
const progress = isProgressEvent(statusEntry) ? statusEntry : undefined;
|
||||
|
||||
results.push(...monitor.action(action, progress));
|
||||
}
|
||||
|
||||
// deliver results as a stream containing individual records to maintain
|
||||
|
||||
@@ -2,19 +2,19 @@ import { Subject } from "rxjs";
|
||||
|
||||
import { EventFormat } from "../log/ecs-format";
|
||||
|
||||
import { Achievement, AchievementFormat, AchievementWatch } from "./types";
|
||||
import { Achievement, AchievementEvent, AchievementValidator } from "./types";
|
||||
|
||||
// sync data from the server (consumed by event store)
|
||||
const replicationIn$ = new Subject<AchievementFormat>();
|
||||
const replicationIn$ = new Subject<AchievementEvent>();
|
||||
|
||||
// data incoming from the UI (consumed by validator)
|
||||
const userActionIn$ = new Subject<EventFormat>();
|
||||
|
||||
// what to look for (consumed by validator)
|
||||
const achievementMonitors$ = new Subject<AchievementWatch[]>();
|
||||
const achievementMonitors$ = new Subject<AchievementValidator[]>();
|
||||
|
||||
// data stored in local state (consumed by validator and achievement list)
|
||||
const achievementsLocal$ = new Subject<AchievementFormat[]>();
|
||||
const achievementsLocal$ = new Subject<AchievementEvent[]>();
|
||||
|
||||
// metadata (consumed by achievement list)
|
||||
const achievementMetadata$ = new Subject<Achievement>();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Earned, Progress } from "./types";
|
||||
import { AchievementEarnedEvent, AchievementProgressEvent } from "./types";
|
||||
|
||||
function isProgress(achievement: any): achievement is Progress {
|
||||
function isProgressEvent(achievement: any): achievement is AchievementProgressEvent {
|
||||
return achievement.type === "progress" && "value" in achievement;
|
||||
}
|
||||
|
||||
function isEarned(achievement: any): achievement is Earned {
|
||||
return !isProgress(achievement);
|
||||
function isEarnedEvent(achievement: any): achievement is AchievementEarnedEvent {
|
||||
return !isProgressEvent(achievement);
|
||||
}
|
||||
|
||||
export { isProgress, isEarned };
|
||||
export { isProgressEvent, isEarnedEvent };
|
||||
|
||||
@@ -3,15 +3,23 @@ import { Tagged } from "type-fest/source/opaque";
|
||||
|
||||
import { EventFormat, ServiceFormat } from "../log/ecs-format";
|
||||
|
||||
export type AchievementId = string & Tagged<"achievement">;
|
||||
import { Type } from "./data";
|
||||
|
||||
type Progress = { type: "progress"; name: AchievementId; value: number };
|
||||
type Earned = { type: "earned"; name: AchievementId };
|
||||
export type AchievementFormat = EventFormat & ServiceFormat & { achievement: Progress | Earned };
|
||||
export type EvaluatorType = keyof typeof Type;
|
||||
export type AchievementId = string & Tagged<"achievement">;
|
||||
export type AchievementProgressId = string & Tagged<"achievement-progress">;
|
||||
|
||||
export type AchievementProgressEvent = EventFormat &
|
||||
ServiceFormat & { achievement: { type: "progress"; name: AchievementProgressId; value: number } };
|
||||
export type AchievementEarnedEvent = EventFormat &
|
||||
ServiceFormat & { achievement: { type: "earned"; name: AchievementId } };
|
||||
export type AchievementEvent = AchievementProgressEvent | AchievementEarnedEvent;
|
||||
|
||||
// consumed by validator and achievement list (should this include a "toast-alerter"?)
|
||||
export type Achievement = {
|
||||
achievement: AchievementId;
|
||||
progress: AchievementProgressId;
|
||||
evaluator: EvaluatorType;
|
||||
|
||||
// pre-filter that disables the rule if it's met
|
||||
trigger: "once" | RequireAtLeastOne<{ low: number; high: number }>;
|
||||
@@ -20,13 +28,13 @@ export type Achievement = {
|
||||
};
|
||||
|
||||
// consumed by validator
|
||||
export type AchievementWatch = Achievement & {
|
||||
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?: AchievementFormat,
|
||||
) => [AchievementFormat] | [AchievementFormat, AchievementFormat];
|
||||
progress?: AchievementProgressEvent,
|
||||
) => [AchievementEvent] | [AchievementEvent, AchievementEvent];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user