Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225import type { AutomergeUrl, Repository } from '../storage/document-runtime-contract'import { App as PluginApp, Vault, Workspace } from '../plugin'import { DocumentManager } from '../services/document-manager'import { FileIndex } from '../services/file-index'import { VaultUserEntries } from '../services/vault-user-entries'import { VaultStateRoot } from '../services/vault-state-root'import { VaultStateData } from '../services/vault-state-data'import { VaultStateMap } from '../services/vault-state-map'import { VaultPreferences } from '../services/vault-preferences'import { RecentVisits } from '../services/recent-visits'import { EnabledStateStore } from '../plugin/loader/enabled-state-store'import { ATPROTO_VAULT_ROOT_URL_KEY, getOrCreateVaultRootUrl, vaultRootUrlKeyForEdition } from '../services/vault-root-url'import { OnboardingState } from '../services/onboarding-state'import { installOnboarding } from '../services/install-onboarding'import { refreshWelcome } from '../services/refresh-welcome'import type { ReamsEdition } from '../editions/types'import type { EditionAccountUiHook, EditionFeedbackModal, EditionPublishContribution, EditionSearchContribution, EditionSyncSettingsSection } from '../editions/contributions'import type { RepositoryInvariantProvider } from '../testing/invariants/reams-invariant-diagnostics'import { installStarterTemplate } from '../plugins/templates/starter-template'import { DAILY_STARTER_TEMPLATE_CONTENT, STARTER_TEMPLATE_CONTENT } from '../plugins/templates/starter-template-content'
export interface AppRuntime { readonly edition: ReamsEdition readonly documentManager: DocumentManager readonly pluginApp: PluginApp readonly rootUrl: AutomergeUrl readonly stateRoot: VaultStateRoot readonly stateData: VaultStateData readonly enabledStateStore: EnabledStateStore readonly preferences: VaultPreferences readonly recentVisits: RecentVisits readonly vaultRootStorageKey: string readonly accountUi: EditionAccountUiHook readonly syncSettingsSection?: EditionSyncSettingsSection readonly searchContribution?: EditionSearchContribution readonly feedbackModal?: EditionFeedbackModal readonly publishContribution?: EditionPublishContribution readonly repositoryDiagnostics?: RepositoryInvariantProvider readonly backgroundReady?: Promise<void> startBackgroundWork(): void openVault(options: { signal: AbortSignal; onStage?: (stage: 'opening-vault') => void; mark?: (step: string) => void }): Promise<boolean> reattach(): void initializeStarterTemplate(): Promise<void> close(): void}
export function createAppRuntime(repo: Repository, options: { edition?: ReamsEdition vaultRootStorageKey?: string rootUrl?: AutomergeUrl localStorageScope?: string accountUi: EditionAccountUiHook syncSettingsSection?: EditionSyncSettingsSection searchContribution?: EditionSearchContribution feedbackModal?: EditionFeedbackModal publishContribution?: EditionPublishContribution repositoryDiagnostics?: RepositoryInvariantProvider}): AppRuntime { const edition = options.edition ?? 'classic' const vaultRootStorageKey = options.vaultRootStorageKey ?? vaultRootUrlKeyForEdition(edition) const accountVault = edition === 'atproto' && vaultRootStorageKey === ATPROTO_VAULT_ROOT_URL_KEY const documentManager = new DocumentManager(repo, edition === 'atproto' && !accountVault ? 'anonymous' : undefined) const isFreshVault = !options.rootUrl && !localStorage.getItem(vaultRootStorageKey) const rootUrl = options.rootUrl ?? getOrCreateVaultRootUrl(documentManager, vaultRootStorageKey) // Only roots created here are eligible. Retain the pending marker across a // reload if startup fails between creating the file and saving its settings. const starterTemplatePendingKey = `${vaultRootStorageKey}:starter-template-pending` if (isFreshVault) localStorage.setItem(starterTemplatePendingKey, rootUrl) const userEntries = new VaultUserEntries(rootUrl, documentManager) const stateRoot = new VaultStateRoot(documentManager, rootUrl) const stateData = new VaultStateData(stateRoot, documentManager) const enabledStateMap = new VaultStateMap(stateRoot, documentManager, 'plugin-enabled.json') const enabledStateStore = new EnabledStateStore(enabledStateMap) const preferences = new VaultPreferences(new VaultStateMap(stateRoot, documentManager, 'preferences.json')) const recentVisits = new RecentVisits(new VaultStateMap(stateRoot, documentManager, 'recent-visits.json')) const fileIndex = new FileIndex(documentManager, rootUrl, { userEntries, findFolderHandle: (url) => documentManager.getFolderHandle(url), findFileHandle: (url) => documentManager.findFileHandle(url, { priority: 'background' }), deferInitialStats: true, }) const vault = new Vault('Default', fileIndex, documentManager, userEntries) const workspace = new Workspace(document.body, (file) => fileIndex.getUrlForFile(file) ?? file.path) const pluginApp = new PluginApp(vault, workspace, options.localStorageScope ? { storageScope: options.localStorageScope } : undefined) pluginApp.setDocumentManager(documentManager) pluginApp.reamsStateData = stateData let starterTemplatePromise: Promise<void> | undefined let welcomeRefreshPromise: Promise<void> | undefined let welcomeRefreshController: AbortController | undefined let closed = false let openingGeneration = 0
return { edition, documentManager, pluginApp, rootUrl, stateRoot, stateData, enabledStateStore, preferences, recentVisits, vaultRootStorageKey, accountUi: options.accountUi, syncSettingsSection: options.syncSettingsSection, searchContribution: options.searchContribution, feedbackModal: options.feedbackModal, publishContribution: options.publishContribution, repositoryDiagnostics: options.repositoryDiagnostics, backgroundReady: repo.backgroundReady, startBackgroundWork() { if (closed) return repo.startBackgroundSync?.() if (!welcomeRefreshPromise) { const controller = new AbortController() welcomeRefreshController = controller welcomeRefreshPromise = refreshWelcome(pluginApp, documentManager, () => repo.flushLocal ? repo.flushLocal() : repo.flush(), controller.signal) .catch(error => { if (!controller.signal.aborted) console.warn('Welcome refresh deferred until the next opening:', error) if (welcomeRefreshController === controller) welcomeRefreshPromise = undefined }) } }, async openVault({ signal, onStage, mark }) { if (signal.aborted || closed) return false const generation = ++openingGeneration const current = () => !signal.aborted && generation === openingGeneration && !closed const started = performance.now() const timings: Record<string, number> = {} const saveTimings = () => { if (current()) { try { sessionStorage.setItem('reams:vault-startup-timings', JSON.stringify(timings)) } catch { /* optional duration-only diagnostics */ } } } const measure = async <T,>(step: string, work: () => Promise<T>): Promise<T> => { const start = performance.now() try { return await work() } finally { timings[step] = Math.round(performance.now() - start); saveTimings() } } const trace = (step: string) => { if (current()) mark?.(step) return current() } onStage?.('opening-vault') if (!current()) return false if (!trace('vault-start')) return false const folder = await measure('root-validation', () => documentManager.getFolder(rootUrl)) if (!current()) return false if (!trace('root-validated')) return false if (!folder) { if (edition === 'atproto' && vaultRootStorageKey === ATPROTO_VAULT_ROOT_URL_KEY) { throw new Error(`Authoritative remote vault root ${rootUrl} is unavailable; refusing to create a replacement.`) } console.warn(`Vault root ${rootUrl} is invalid or corrupted — creating fresh root`) if (!current()) return false const replacement = documentManager.createFolder() if (!current()) return false localStorage.setItem(vaultRootStorageKey, replacement) if (current()) window.location.reload() return false } const stateReady = measure('state-root', () => stateRoot.open()).then(async () => { trace('state-root-opened') if (!current()) return await Promise.all([ measure('preferences', () => preferences.open()).then(() => trace('preferences-opened')), measure('recent-visits', () => recentVisits.open()).then(() => trace('recent-visits-opened')), measure('enabled-plugins', () => enabledStateStore.ready()).then(() => trace('enabled-plugins-loaded')), ]) }) await Promise.all([measure('file-index', () => pluginApp.vault.init()).then(() => trace('file-index-hydrated')), stateReady]) if (!current()) return false await measure('starter-templates', () => this.initializeStarterTemplate()) if (!current()) return false pluginApp.reamsRibbon.reloadConfig() pluginApp.reamsStatusBar.reloadConfig() pluginApp.reamsFormattingActions.reloadConfig() timings.total = Math.round(performance.now() - started) saveTimings() return current() }, reattach() { closed = false; pluginApp.reattach() }, initializeStarterTemplate() { if (!accountVault && localStorage.getItem(starterTemplatePendingKey) !== rootUrl) return Promise.resolve() if (!starterTemplatePromise) { starterTemplatePromise = (async () => { const onboarding = new OnboardingState(documentManager, rootUrl) try { if (accountVault && !await onboarding.pending()) { await onboarding.complete(); return } await installStarterTemplate(pluginApp, async (notes, daily) => { await documentManager.createOrOpenStarterTemplates(rootUrl, [ ...(notes ? [{ name: 'default.md', content: STARTER_TEMPLATE_CONTENT }] : []), ...(daily ? [{ name: 'daily.md', content: DAILY_STARTER_TEMPLATE_CONTENT }] : []), ]) await vault.init() }) await installOnboarding(documentManager, rootUrl) await onboarding.completeWelcomeRefresh() if (accountVault) await onboarding.complete() await vault.init() } finally { onboarding.close() } })().then(() => { localStorage.removeItem(starterTemplatePendingKey) }).catch(error => { starterTemplatePromise = undefined throw error }) } return starterTemplatePromise }, close() { closed = true welcomeRefreshController?.abort() welcomeRefreshPromise = undefined openingGeneration++ enabledStateStore.close() enabledStateMap.close() preferences.close() recentVisits.close() stateData.close() pluginApp.destroy() }, }}