124 lines · 4.9 KB · TypeScript
RawDownload
import * as Effect from "effect/Effect";import { AsyncResult, Atom } from "effect/unstable/reactivity"; import { MobilePreferencesStore, type Preferences } from "../persistence/mobile-preferences";import * as Runtime from "../lib/runtime"; export {  MobilePreferencesLoadError,  MobilePreferencesSaveError,  MobilePreferencesStore,} from "../persistence/mobile-preferences"; interface OptimisticPreferences {  readonly values: Partial<Preferences>;  readonly versions: Partial<Record<keyof Preferences, number>>;} /** * Owns the device preference blob for the lifetime of the app registry. * Optimistic patches are kept separately so writes made while persistence is * still loading cannot be replaced by the eventual read result. */export function createMobilePreferencesState(runtime: Atom.AtomRuntime<MobilePreferencesStore>) {  const storedPreferencesAtom = runtime    .atom(      MobilePreferencesStore.pipe(        Effect.flatMap((store) => store.load),        Effect.catch((error) =>          Effect.logWarning("Could not load mobile preferences.", error).pipe(            Effect.as<Preferences>({}),          ),        ),      ),    )    .pipe(Atom.keepAlive, Atom.withLabel("mobile:preferences:stored"));   const optimisticPatchAtom = Atom.make<OptimisticPreferences>({ values: {}, versions: {} }).pipe(    Atom.keepAlive,    Atom.withLabel("mobile:preferences:optimistic-patch"),  );  const confirmedPreferencesAtom = Atom.make<Preferences>({}).pipe(    Atom.keepAlive,    Atom.withLabel("mobile:preferences:confirmed"),  );  let nextPatchVersion = 0;   const preferencesAtom = Atom.make((get) => {    const stored = get(storedPreferencesAtom);    const confirmed = get(confirmedPreferencesAtom);    const optimistic = get(optimisticPatchAtom);    return AsyncResult.map(stored, (preferences) => ({      ...preferences,      ...confirmed,      ...optimistic.values,    }));  }).pipe(Atom.keepAlive, Atom.withLabel("mobile:preferences"));   const updatePreferencesAtom = runtime    .fn(      (patch: Partial<Preferences>, get) => {        const version = ++nextPatchVersion;        const current = get(optimisticPatchAtom);        const versions = { ...current.versions };        for (const key of Object.keys(patch) as Array<keyof Preferences>) {          versions[key] = version;        }        get.set(optimisticPatchAtom, {          values: { ...current.values, ...patch },          versions,        });        return MobilePreferencesStore.pipe(          Effect.flatMap((store) => store.savePatch(patch)),          Effect.tap((saved) =>            Effect.sync(() => {              get.set(confirmedPreferencesAtom, saved);              const optimistic = get(optimisticPatchAtom);              const values = { ...optimistic.values } as Record<string, unknown>;              const currentVersions = { ...optimistic.versions } as Record<string, unknown>;              for (const key of Object.keys(patch) as Array<keyof Preferences>) {                if (optimistic.versions[key] === version) {                  delete values[key];                  delete currentVersions[key];                }              }              get.set(optimisticPatchAtom, {                values: values as Partial<Preferences>,                versions: currentVersions as Partial<Record<keyof Preferences, number>>,              });            }),          ),          Effect.tapError(() =>            Effect.sync(() => {              const optimistic = get(optimisticPatchAtom);              const values = { ...optimistic.values } as Record<string, unknown>;              const currentVersions = { ...optimistic.versions } as Record<string, unknown>;              for (const key of Object.keys(patch) as Array<keyof Preferences>) {                if (optimistic.versions[key] === version) {                  delete values[key];                  delete currentVersions[key];                }              }              get.set(optimisticPatchAtom, {                values: values as Partial<Preferences>,                versions: currentVersions as Partial<Record<keyof Preferences, number>>,              });            }),          ),        );      },      // The storage layer serializes preference read-modify-write operations.      // Keep every invocation alive so one preference update cannot interrupt      // another update to a different field in the shared blob.      { concurrent: true },    )    .pipe(Atom.keepAlive, Atom.withLabel("mobile:preferences:update"));   return { preferencesAtom, updatePreferencesAtom } as const;} const mobilePreferencesRuntime = Atom.runtime(Runtime.runtimeContextLayer);export const mobilePreferencesState = createMobilePreferencesState(mobilePreferencesRuntime); export const mobilePreferencesAtom = mobilePreferencesState.preferencesAtom;export const updateMobilePreferencesAtom = mobilePreferencesState.updatePreferencesAtom;