import { MODES } from '../constants'; import type { AdapterWriteFailure, CoveringUniwindAdapterInput, ManagedTargetDefinition, ModeTokenState, PlanThemeOverrideCover, ThemeOverrideCoverPlan, ThemeOverrideSessionState, ThemeOverrideWriterOwnershipToken, TokenPath, } from '../override/contracts'; import type { Mode, Platform } from '../types'; import { mapTokenWritesToUniwind, UniwindAdapterContractError } from './uniwind'; let ownerSequence = 0; export type ThemeOverrideSessionInvariant = | 'writer-owner-mismatch' | 'platform-mismatch' | 'artifact-revision-mismatch' | 'missing-resolved-cover-value'; export class ThemeOverrideSessionInvariantError extends Error { readonly invariant: ThemeOverrideSessionInvariant; readonly mode?: Mode; readonly tokenPath?: TokenPath; constructor( invariant: ThemeOverrideSessionInvariant, message: string, mode?: Mode, tokenPath?: TokenPath, ) { super(message); this.name = 'ThemeOverrideSessionInvariantError'; this.invariant = invariant; this.mode = mode; this.tokenPath = tokenPath; } } export interface CreateThemeOverrideSessionStateOptions { readonly platform: Platform; readonly writerOwner: ThemeOverrideWriterOwnershipToken; } export type ApplyCoveringThemeOverrideResult = | { readonly applied: true; readonly revision: string; readonly session: ThemeOverrideSessionState; readonly plan: ThemeOverrideCoverPlan; } | { readonly applied: false; readonly failure: AdapterWriteFailure; }; function appendUnique(first: readonly TokenPath[], second: readonly TokenPath[]): TokenPath[] { const result = [...first]; const seen = new Set(first); for (const key of second) { if (seen.has(key)) continue; seen.add(key); result.push(key); } return result; } export function createThemeOverrideWriterOwnershipToken( label = 'theme-override-session', ): ThemeOverrideWriterOwnershipToken { if (!label.trim()) { throw new Error('Theme override writer ownership labels must be non-empty strings.'); } ownerSequence += 1; const randomId = [Date.now(), ownerSequence].map(String).join('-'); return `${label}:${randomId}` as ThemeOverrideWriterOwnershipToken; } export function createThemeOverrideSessionState({ platform, writerOwner, }: CreateThemeOverrideSessionStateOptions): ThemeOverrideSessionState { return { everWrittenKeys: { light: [], dark: [], }, platform, lastArtifactRevision: null, writerOwner, }; } export function buildStaticManagedTargets(definitions: readonly ManagedTargetDefinition[]): { readonly light: readonly TokenPath[]; readonly dark: readonly TokenPath[]; } { const targets = appendUnique( [], definitions.map(({ tokenPath }) => tokenPath), ); return { light: targets, dark: [...targets], }; } function assertSessionInvariants( state: ThemeOverrideSessionState, writerOwner: ThemeOverrideWriterOwnershipToken, platform: Platform, artifactRevision: string, ): void { if (state.writerOwner !== writerOwner) { throw new ThemeOverrideSessionInvariantError( 'writer-owner-mismatch', 'The covering session writer owner does not own this Uniwind store.', ); } if (state.platform !== platform) { throw new ThemeOverrideSessionInvariantError( 'platform-mismatch', `The covering session is bound to ${state.platform}, not ${platform}.`, ); } if (state.lastArtifactRevision !== null && state.lastArtifactRevision !== artifactRevision) { throw new ThemeOverrideSessionInvariantError( 'artifact-revision-mismatch', `The covering session revision changed from "${state.lastArtifactRevision}" to "${artifactRevision}".`, ); } } export const planThemeOverrideCover: PlanThemeOverrideCover = ({ expanded, state, staticManagedTargets, writerOwner, }) => { assertSessionInvariants(state, writerOwner, expanded.platform, expanded.revision.id); const coverKeys = { light: appendUnique(staticManagedTargets.light, state.everWrittenKeys.light), dark: appendUnique(staticManagedTargets.dark, state.everWrittenKeys.dark), }; const writes = { light: { ...expanded.writes.light }, dark: { ...expanded.writes.dark }, }; for (const mode of MODES) { for (const tokenPath of coverKeys[mode]) { if (Object.hasOwn(writes[mode], tokenPath)) continue; if (!Object.hasOwn(expanded.resolved[mode], tokenPath)) { throw new ThemeOverrideSessionInvariantError( 'missing-resolved-cover-value', `Expanded ${mode} state has no concrete fallback for covered target "${tokenPath}".`, mode, tokenPath, ); } writes[mode][tokenPath] = expanded.resolved[mode][tokenPath]; } } return { coverKeys, writes, nextState: { everWrittenKeys: { light: appendUnique(state.everWrittenKeys.light, Object.keys(writes.light)), dark: appendUnique(state.everWrittenKeys.dark, Object.keys(writes.dark)), }, platform: state.platform, lastArtifactRevision: expanded.revision.id, writerOwner: state.writerOwner, }, }; }; function adapterFailure(mode: Mode, artifactRevision: string, cause: unknown): AdapterWriteFailure { return { code: 'override.adapter_write_failed', mode, artifactRevision, cause, recovery: 'reload-and-reapply', }; } export function applyCoveringThemeOverride( input: CoveringUniwindAdapterInput, ): ApplyCoveringThemeOverrideResult { const { expanded, runtimeMap, writer } = input; let plan: ThemeOverrideCoverPlan; try { plan = planThemeOverrideCover({ expanded, state: input.session, staticManagedTargets: input.staticManagedTargets, writerOwner: input.writerOwner, }); } catch (cause) { const mode = cause instanceof ThemeOverrideSessionInvariantError ? (cause.mode ?? 'light') : 'light'; return { applied: false, failure: adapterFailure(mode, expanded.revision.id, cause), }; } let maps: ModeTokenState; try { maps = mapTokenWritesToUniwind({ platform: expanded.platform, writes: plan.writes, runtimeMap, }); } catch (cause) { const mode = cause instanceof UniwindAdapterContractError ? cause.mode : 'light'; return { applied: false, failure: adapterFailure(mode, expanded.revision.id, cause), }; } for (const mode of MODES) { if (Object.keys(maps[mode]).length === 0) continue; try { writer(mode, maps[mode]); } catch (cause) { return { applied: false, failure: adapterFailure(mode, expanded.revision.id, cause), }; } } return { applied: true, revision: expanded.revision.id, session: plan.nextState, plan, }; }