// Persistence: the React surface. `KaleidoscopeStateProvider` hydrates the // stored selection once at mount and writes through on every change; // `useKaleidoscopeState` hands the host the hydrated values plus the setters. // // The provider owns STORAGE state only; it never binds a track or calls the // verbs. The host applies the restored selection itself (gated on `hydrated`, // so a stored preset is not flashed over by the default): // // // `controls` is the binding from bindKaleidoscope(track, { presets }). // const { hydrated, presetId, patchesFor, mask, ... } = useKaleidoscopeState(); // useEffect(() => { // if (!hydrated || !controls) return; // // patchesFor reads this render's patches; live edits go through onPatch, // // not this re-apply, so it stays out of the deps. // if (presetId) controls.kaleidoscope(presetId, patchesFor(presetId)); // else controls.kaleidoscope(null); // }, [hydrated, controls, presetId]); // // The backing store defaults to AsyncStorage; pass any `KaleidoscopeStateStore` // to swap it. Importing this subpath is what brings the optional // `@react-native-async-storage/async-storage` peer onto your bundle path // (Metro resolves it at bundle time either way, so there is no lazy escape). import { createContext, type ReactElement, type ReactNode, useContext, useEffect, useRef, useState, } from 'react'; import type { MaskInput, PatchesFor } from '../kaleidoscope/types'; import type { KaleidoscopePresetBook } from '../kaleidoscope.preset-book.types'; import { kaleidoscopeAsyncStorageStore } from './async-storage-store'; import { DEFAULT_MASK, type KaleidoscopeStateStore, mergePatch, patchListFor, pruneStoredState, type StoredPatch, type StoredPatches, } from './state'; export type KaleidoscopeStateValue

= { /** False until the persisted selection has been read; apply no effects before then. */ readonly hydrated: boolean; /** The selected preset id, or null when nothing is selected. */ readonly presetId: (keyof P & string) | null; /** The shared segmentation edge. */ readonly mask: MaskInput; /** Every preset's stored per-layer overrides (keyed by preset id, then layer id). */ readonly patches: StoredPatches; readonly setPreset: (presetId: (keyof P & string) | null) => void; readonly setMask: (mask: MaskInput) => void; /** Record one control-panel patch against a preset (and persist it). */ readonly setPatch: (presetId: keyof P & string, patch: StoredPatch) => void; /** A preset's stored overrides in the array shape `kaleidoscope(id, patches)` takes. */ readonly patchesFor: (presetId: K) => PatchesFor; /** Clear the stored selection back to defaults (and persist the cleared state). */ readonly reset: () => void; }; const KaleidoscopeStateContext = createContext(null); export type KaleidoscopeStateProviderProps

= { /** The consumer's preset book; stored state is pruned against it at hydrate. */ readonly presets: P; /** The backing store. Defaults to the AsyncStorage store (lazily loaded). */ readonly store?: KaleidoscopeStateStore; /** The mask used before hydration and after `reset`. Defaults to 0.5/0.5. */ readonly defaultMask?: MaskInput; readonly children: ReactNode; }; type Selection = { readonly presetId: string | null; readonly mask: MaskInput; readonly patches: StoredPatches; }; export function KaleidoscopeStateProvider

({ presets, store, defaultMask = DEFAULT_MASK, children, }: KaleidoscopeStateProviderProps

): ReactElement { const [hydrated, setHydrated] = useState(false); const [selection, setSelection] = useState({ presetId: null, mask: defaultMask, patches: {}, }); // A write before hydration wins over the stored value (the person acted; do // not clobber their fresh choice with yesterday's). const dirty = useRef(false); // Pinned for the provider's lifetime: hydrate and every write-through go to // the same store the first render saw. const storeRef = useRef(store ?? kaleidoscopeAsyncStorageStore); // biome-ignore lint/correctness/useExhaustiveDependencies: hydrate exactly once; the book and store are bind-time constants. useEffect(() => { let cancelled = false; storeRef.current.load().then( (stored) => { if (cancelled) return; if (stored && !dirty.current) { const pruned = pruneStoredState(stored, presets); setSelection({ presetId: pruned.presetId, mask: pruned.mask, patches: pruned.patches }); } setHydrated(true); }, () => { if (!cancelled) setHydrated(true); }, ); return () => { cancelled = true; }; }, []); const commit = (next: Selection): void => { dirty.current = true; setSelection(next); void storeRef.current.save({ version: 1, ...next }); }; const value: KaleidoscopeStateValue = { hydrated, presetId: selection.presetId, mask: selection.mask, patches: selection.patches, setPreset: (presetId) => commit({ ...selection, presetId }), setMask: (mask) => commit({ ...selection, mask }), setPatch: (presetId, patch) => commit({ ...selection, patches: mergePatch(selection.patches, presetId, patch) }), // The stored wire shape is book-agnostic; the typed view is recovered at the // hook (`useKaleidoscopeState()`), so the cast is the seam. patchesFor: ((presetId: string) => patchListFor(selection.patches, presetId)) as KaleidoscopeStateValue['patchesFor'], reset: () => commit({ presetId: null, mask: defaultMask, patches: {} }), }; return ( {children} ); } /** * The persisted selection plus its setters. Typed by the consumer's book: * `useKaleidoscopeState()`. Throws outside the provider. */ export function useKaleidoscopeState< P extends KaleidoscopePresetBook = KaleidoscopePresetBook, >(): KaleidoscopeStateValue

{ const value = useContext(KaleidoscopeStateContext); if (value === null) { throw new Error( 'useKaleidoscopeState: no above this component. ' + 'Wrap your app (or the screen using the picker) in the provider from ' + "'react-native-webrtc-kaleidoscope/persistence'.", ); } // Safe by construction: the provider pruned ids against the same book the // consumer parameterizes with; the runtime shapes are identical. return value as unknown as KaleidoscopeStateValue

; }