Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677import { createAutomergeAtproto } from 'automerge-atproto'import { EditorState, Annotation } from '@codemirror/state'import { EditorView } from '@codemirror/view'import * as A from '@automerge/automerge'
const remoteProjection = Annotation.define()let api, descriptor, runtime, note, view, noteChanges = 0const id = url => url.replace(/^automerge:/, '')const file = (name, content) => ({ '@patchwork': { type: 'file' }, name, content })const folder = docs => ({ '@patchwork': { type: 'folder' }, docs })const entry = (name, type, handle) => ({ name, type, url: `automerge:${handle.documentId}` })
window.__progressive = { async setup(config) { api = await createAutomergeAtproto({ storageName: 'progressive-browser-proof', scope: config.scope, oauth: { metadata: { client_id: config.clientId, redirect_uri: config.callback }, identityResolver: { resolve: async () => ({ did: config.did, handle: config.handle, pds: config.pds }) } }, provider: { endpoint: config.provider, clientKey: config.clientKey }, ...(config.acceptCreationDid ? { contributionPolicy: { accepts: ({ kind, authorDid, ownerDid }) => kind === 'creation' ? authorDid === config.acceptCreationDid : kind === 'content' || kind === 'tombstone' && authorDid === ownerDid } } : {}), }) }, startLogin: hint => api.startLogin({ loginHint: hint }), completeLogin: input => api.completeLogin(input), restore: () => api.restore().then(() => true), async seed() { descriptor = await api.createSpace({ type: 'xyz.sealight.automerge.test', key: 'progressive' }) runtime = await api.openSpace(descriptor, { recovery: 'on-demand' }) note = runtime.documents.create({ initialize: doc => Object.assign(doc, file('selected.md', 'Selected cold note')) }) const large = runtime.documents.create({ initialize: doc => Object.assign(doc, file('unrelated.md', 'Unrelated history')) }) const nested = runtime.documents.create({ initialize: doc => Object.assign(doc, folder([entry('selected.md', 'file', note)])) }) const root = runtime.documents.create({ initialize: doc => Object.assign(doc, folder([entry('Nested', 'folder', nested), entry('unrelated.md', 'file', large)])) }) const deleted = runtime.documents.create({ initialize: doc => Object.assign(doc, file('deleted.md', 'Deleted fixture')) }) const compound = runtime.documents.create({ initialize: doc => Object.assign(doc, file('compound.md', 'Compound fixture')) }) const branch = runtime.documents.import(A.save(A.from({ mergedBranch: true }, { actor: '0'.repeat(32) }))) compound.merge(branch) await runtime.flush() for (let revision = 1; revision <= 24; revision++) { large.change(doc => { doc.revision = revision }); await runtime.flush() } await deleted.delete(); await runtime.flush() return { descriptor, rootId: root.documentId, noteId: note.documentId, largeId: large.documentId, deletedId: deleted.documentId, compoundId: compound.documentId } }, async open(space) { descriptor = space; runtime = await api.openSpace(space, { recovery: 'on-demand' }); return runtime.documents.list().length }, async openSelected(rootId) { const root = await runtime.documents.load(rootId) const nested = await runtime.documents.load(id(root.doc().docs.find(item => item.type === 'folder').url)) const reference = nested.doc().docs[0] document.querySelector('#tree').textContent = root.doc().docs.map(item => item.name).join(' / ') + ' / ' + reference.name note = await runtime.documents.load(id(reference.url)) view?.destroy() view = new EditorView({ parent: document.querySelector('#editor'), state: EditorState.create({ doc: note.doc().content }) }) note.onChange(({ doc }) => { noteChanges++ if (view && view.state.doc.toString() !== doc.content) view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: doc.content }, annotations: remoteProjection.of(true) }) }) return { cached: runtime.documents.list().length, content: view.state.doc.toString() } }, async loadCode(documentId) { try { await runtime.documents.load(documentId); return 'ready' } catch (error) { return error.code ?? error.name } }, async revision(documentId) { return (await runtime.documents.load(documentId)).doc().revision }, async createLater() { const handle = runtime.documents.create({ initialize: doc => Object.assign(doc, file('later.md', 'Created after inventory')) }); await runtime.flush(); return handle.documentId }, async installRace(documentId) { runtime.documents.install({ documentId, binary: A.save(A.from(file('epoch-race.md', 'Current epoch creation'))) }); await runtime.flush() }, async editDocument(documentId) { const handle = await runtime.documents.load(documentId); handle.change(doc => { doc.peerEdit = true }); await runtime.flush(); return handle.doc().mergedBranch === true }, async remoteEdit() { note.change(doc => { doc.content += '\nRemote fixture edit' }); await runtime.flush() }, async localPending(value = 'retained') { note.change(doc => { doc.localPending = value }); await runtime.flushLocal() }, async refreshField() { await runtime.sync(); return note.doc().localPending }, async catchUp() { const previous = note await runtime.sync() const current = await runtime.documents.load(note.documentId) return { sameHandle: previous === current, pending: current.metrics().pendingChanges, localPending: current.doc().localPending, changes: noteChanges, content: view.state.doc.toString() } }, pending: () => note.metrics().pendingChanges, async flush() { await runtime.flush() }, async closeSpace() { view?.destroy(); view = undefined; await runtime.close() },}