Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105import type { VaultStateMap } from '../../services/vault-state-map'
/** Synchronous plugin lifecycle projection over the host's synced state. */export class EnabledStateStore { private state: Record<string, boolean> = {} /** Whether state has ever been modified (distinguishes first-run from all-disabled) */ private touched = false private unsubscribe: (() => void) | null = null private listeners = new Set<() => void>() private readonly pending = new Map<string, string>() private readonly pendingTokens = new Map<string, symbol>()
constructor(private readonly synced?: VaultStateMap) {}
/** Must complete before default built-ins and external plugins are enabled. */ async ready(): Promise<void> { if (!this.synced) return await this.synced.open() const refresh = (values: Readonly<Record<string, string>>) => { const next: Record<string, boolean> = {} for (const [id, value] of Object.entries(values)) { if (value !== 'true' && value !== 'false') throw new Error(`Malformed enabled state for ${id}`) next[id] = value === 'true' } // Keep synchronous startup defaults visible while their individual // writes pass through asynchronous handle discovery. A partial local // change event must not temporarily disable the other built-ins. for (const [id, value] of this.pending) { if (values[id] === value) { this.pending.delete(id); this.pendingTokens.delete(id) } else next[id] = value === 'true' } const changed = JSON.stringify(this.state) !== JSON.stringify(next) this.state = next this.touched = Object.keys(next).length > 0 if (changed) for (const listener of this.listeners) listener() } refresh(this.synced.snapshot()) this.unsubscribe ??= this.synced.subscribe(refresh) }
subscribe(listener: () => void): () => void { this.listeners.add(listener) return () => { this.listeners.delete(listener) } }
/** * Check if a plugin is enabled. * Returns false for unknown or explicitly disabled plugins. */ isEnabled(pluginId: string): boolean { return this.state[pluginId] === true }
/** * Set enabled state for a plugin. */ setEnabled(pluginId: string, enabled: boolean): void { this.touched = true // Store both true and false explicitly so we can distinguish // "user disabled this" from "never seen this plugin before". this.state[pluginId] = enabled if (this.synced) { // The map change is synchronous once opened; report persistence failures // rather than silently treating a device-local mirror as authoritative. this.pending.set(pluginId, String(enabled)) const token = Symbol(pluginId) this.pendingTokens.set(pluginId, token) void this.synced.set(pluginId, String(enabled)).catch(error => { if (this.pendingTokens.get(pluginId) !== token) return this.pendingTokens.delete(pluginId) this.pending.delete(pluginId) console.error(`Failed to sync plugin ${pluginId} enabled state:`, error) void this.ready().catch(err => console.error('Could not refresh plugin enabled state:', err)) }) } }
close(): void { this.unsubscribe?.(); this.unsubscribe = null; this.listeners.clear(); this.pending.clear(); this.pendingTokens.clear() }
/** * Get all enabled plugin IDs. */ getEnabledPluginIds(): string[] { return Object.keys(this.state).filter(id => this.state[id]) }
/** * Check if this is a first run (no plugins have ever been enabled/disabled). * Used to determine whether to apply default enabled state for built-ins. */ isEmpty(): boolean { return !this.touched }
/** * Check if a plugin has ever had its state set (enabled or disabled). * Returns false for plugins that have never been seen before. * Used to auto-enable new built-in plugins for existing users. */ hasBeenSeen(pluginId: string): boolean { return pluginId in this.state }
}