Skip to content

Commit

Permalink
Parse previews for local clients
Browse files Browse the repository at this point in the history
  • Loading branch information
benmerckx committed Oct 8, 2024
1 parent 8bb45f6 commit af24254
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 179 deletions.
155 changes: 45 additions & 110 deletions src/adapter/next/cms.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,18 @@
import {Headers} from '@alinea/iso'
import {Database} from 'alinea/backend/Database'
import {EntryResolver} from 'alinea/backend/resolver/EntryResolver'
import {createPreviewParser} from 'alinea/backend/resolver/ParsePreview'
import {generatedStore} from 'alinea/backend/store/GeneratedStore'
import {AuthenticateRequest, Client, ClientOptions} from 'alinea/core/Client'
import {Client} from 'alinea/core/Client'
import {CMS} from 'alinea/core/CMS'
import {Config} from 'alinea/core/Config'
import {EntryRow} from 'alinea/core/EntryRow'
import {outcome} from 'alinea/core/Outcome'
import {
PreviewPayload,
ResolveDefaults,
ResolveParams
} from 'alinea/core/Resolver'
import {PreviewRequest, ResolveParams} from 'alinea/core/Resolver'
import {User} from 'alinea/core/User'
import {assign} from 'alinea/core/util/Objects'
import {getPreviewPayloadFromCookies} from 'alinea/preview/PreviewCookies'
import {decodePreviewPayload} from 'alinea/preview/PreviewPayload'
import * as Y from 'yjs'
import PLazy from 'p-lazy'
import {devUrl, requestContext} from './context.js'

class NextClient extends Client {
resolver: EntryResolver

constructor(private db: Database, options: ClientOptions) {
super(options)
this.resolver = new EntryResolver(db, db.config.schema)
}

async resolve(params: ResolveParams): Promise<unknown> {
const {PHASE_PRODUCTION_BUILD} = await import('next/constants.js')
const isBuild = process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD
if (!params.preview && !isBuild) {
const syncInterval = params.syncInterval ?? 60
const now = Date.now()
if (now - lastSync >= syncInterval * 1000) {
lastSync = now
const db = await database
await db.syncWith(client).catch(() => {})
}
}
return this.resolver.resolve({...resolveDefaults, ...params})
}

async parsePreview(preview: PreviewPayload): Promise<EntryRow | undefined> {
const update = await decodePreviewPayload(preview.payload)
let meta = await db.meta()
if (update.contentHash !== meta.contentHash) {
await syncPending(ctx)
meta = await db.meta()
}
const entry = await resolver.resolve<EntryRow>({
selection: createSelection(Entry({entryId: update.entryId}).maybeFirst()),
realm: Realm.PreferDraft
})
if (!entry) return
const cachedDraft = await drafts.get(update.entryId)
let currentDraft: Draft | undefined
if (cachedDraft?.contentHash === meta.contentHash) {
currentDraft = cachedDraft.draft
} else {
try {
const pending = backend.drafts.get(ctx, update.entryId)
drafts.set(
update.entryId,
pending.then(draft => ({contentHash: meta.contentHash, draft}))
)
currentDraft = await pending
} catch (error) {
console.warn('> could not fetch draft', error)
}
}
const apply = currentDraft
? mergeUpdatesV2([currentDraft.draft, update.update])
: update.update
const type = cms.config.schema[entry.type]
if (!type) return
const doc = new Y.Doc()
Y.applyUpdateV2(doc, apply)
const entryData = parseYDoc(type, doc)
return {...entry, ...entryData, path: entry.path}
}
}

export interface PreviewProps {
widget?: boolean
workspace?: string
Expand All @@ -92,54 +24,57 @@ export class NextCMS<
> extends CMS<Definition> {
constructor(config: Definition, public baseUrl?: string) {
let lastSync = 0
const database = generatedStore.then(
store => new Database(this.config, store)
)
const dbResolver = database.then(
db => new EntryResolver(db, this.config.schema)
const database = PLazy.from(() =>
generatedStore.then(store => new Database(this.config, store))
)
const previewParser = PLazy.from(() => database.then(createPreviewParser))
super(config, async () => {
const context = await requestContext(config)
const resolveDefaults: ResolveDefaults = {}
const {PHASE_PRODUCTION_BUILD} = await import('next/constants.js')
const isBuild = process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD
const {cookies, draftMode} = await import('next/headers.js')
const [isDraft] = outcome(() => draftMode().isEnabled)
const applyAuth: AuthenticateRequest = init => {
const headers = new Headers(init?.headers)
headers.set('Authorization', `Bearer ${context.apiKey}`)
return {...init, headers}
}
const url = context.handlerUrl.href
if (isDraft) {
const cookie = cookies()
const payload = getPreviewPayloadFromCookies(cookie.getAll())
const update = payload && (await decodePreviewPayload(payload))
if (payload) resolveDefaults.preview = {payload}
}
if (devUrl()) return new Client({url, applyAuth, resolveDefaults})
const client = new NextClient(
{
async resolve(params) {
const resolver = await dbResolver
if (!resolveDefaults.preview && !isBuild) {
const client = new Client({
url: context.handlerUrl.href,
applyAuth(init) {
const headers = new Headers(init?.headers)
headers.set('Authorization', `Bearer ${context.apiKey}`)
return {...init, headers}
}
})
const clientResolve = client.resolve.bind(client)
const sync = () => database.then(db => db.syncWith(client))
return assign(client, {
async resolve(params: ResolveParams) {
const isDev = Boolean(devUrl())
let preview: PreviewRequest | undefined
const {cookies, draftMode} = await import('next/headers.js')
const [isDraft] = outcome(() => draftMode().isEnabled)
if (isDraft) {
const cookie = cookies()
const payload = getPreviewPayloadFromCookies(cookie.getAll())
if (payload) preview = {payload}
}
if (isDev) return clientResolve({preview, ...params})
const {PHASE_PRODUCTION_BUILD} = await import('next/constants.js')
const isBuild = process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD
const db = await database
if (!isBuild) {
if (preview) {
const previews = await previewParser
preview = await previews.parse(
preview,
sync,
client.getDraft.bind(client)
)
} else {
const syncInterval = params.syncInterval ?? 60
const now = Date.now()
if (now - lastSync >= syncInterval * 1000) {
lastSync = now
const db = await database
await db.syncWith(client).catch(() => {})
await sync()
}
}
return resolver.resolve({...resolveDefaults, ...params})
}
},
{
url,
applyAuth
return db.resolver.resolve({preview, ...params})
}
)
return client
})
})
}

Expand Down
4 changes: 4 additions & 0 deletions src/backend/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {EntryRecord, createRecord, parseRecord} from 'alinea/core/EntryRecord'
import {createId} from 'alinea/core/Id'
import {Mutation, MutationType} from 'alinea/core/Mutation'
import {PageSeed} from 'alinea/core/Page'
import {Resolver} from 'alinea/core/Resolver'
import {Root} from 'alinea/core/Root'
import {Schema} from 'alinea/core/Schema'
import {EntryUrlMeta, Type} from 'alinea/core/Type'
Expand Down Expand Up @@ -40,6 +41,7 @@ import {Change, ChangeType} from './data/ChangeSet.js'
import {AlineaMeta} from './db/AlineaMeta.js'
import {createEntrySearch} from './db/CreateEntrySearch.js'
import {JsonLoader} from './loader/JsonLoader.js'
import {EntryResolver} from './resolver/EntryResolver.js'

interface Seed {
type: string
Expand All @@ -55,9 +57,11 @@ function seedKey(workspace: string, root: string, filePath: string) {

export class Database implements Syncable {
seed: Map<string, Seed>
resolver: Resolver

constructor(public config: Config, public store: Store) {
this.seed = this.seedData()
this.resolver = new EntryResolver(this, this.config.schema)
}

async syncRequired(contentHash: string): Promise<boolean> {
Expand Down
70 changes: 10 additions & 60 deletions src/backend/Handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,22 @@ import {JWTPreviews} from 'alinea/backend/util/JWTPreviews'
import {cloudBackend} from 'alinea/cloud/CloudBackend'
import {CMS} from 'alinea/core/CMS'
import {Connection} from 'alinea/core/Connection'
import {parseYDoc} from 'alinea/core/Doc'
import {Draft} from 'alinea/core/Draft'
import {Entry} from 'alinea/core/Entry'
import {EntryRow} from 'alinea/core/EntryRow'
import {Graph} from 'alinea/core/Graph'
import {EditMutation, Mutation, MutationType} from 'alinea/core/Mutation'
import {
PreviewPayload,
PreviewUpdate,
ResolveParams,
ResolveRequest
} from 'alinea/core/Resolver'
import {createSelection} from 'alinea/core/pages/CreateSelection'
import {Realm} from 'alinea/core/pages/Realm'
import {Selection} from 'alinea/core/pages/ResolveData'
import {decode} from 'alinea/core/util/BufferToBase64'
import {base64} from 'alinea/core/util/Encoding'
import {assign} from 'alinea/core/util/Objects'
import {decodePreviewPayload} from 'alinea/preview/PreviewPayload'
import {Type, array, enums, object, string} from 'cito'
import PLazy from 'p-lazy'
import pLimit from 'p-limit'
import * as Y from 'yjs'
import {mergeUpdatesV2} from 'yjs'
import {
AuthedContext,
Expand All @@ -34,7 +27,7 @@ import {
RequestContext
} from './Backend.js'
import {ChangeSetCreator} from './data/ChangeSet.js'
import {EntryResolver} from './resolver/EntryResolver.js'
import {createPreviewParser} from './resolver/ParsePreview.js'
import {generatedStore} from './store/GeneratedStore.js'

const limit = pLimit(1)
Expand Down Expand Up @@ -83,15 +76,12 @@ export function createHandler(
): HandlerWithConnect {
const init = PLazy.from(async () => {
const db = await database
const resolver = new EntryResolver(db, cms.schema)
const previews = createPreviewParser(db)
const resolver = db.resolver
const changes = new ChangeSetCreator(
cms.config,
new Graph(cms.config, resolver)
)
const drafts = new Map<
string,
Promise<{contentHash: string; draft?: Draft}>
>()
let lastSync = 0

return {db, mutate, resolve, periodicSync, syncPending}
Expand All @@ -101,10 +91,14 @@ export function createHandler(
await periodicSync(ctx, params.syncInterval)
return resolver.resolve(params as ResolveRequest)
}
const entry = params.preview && (await parsePreview(ctx, params.preview))
const preview = await previews.parse(
params.preview,
() => syncPending(ctx),
entryId => backend.drafts.get(ctx, entryId)
)
return resolver.resolve({
...params,
preview: entry && {entry}
preview: preview
})
}

Expand Down Expand Up @@ -181,51 +175,7 @@ export function createHandler(
}
await backend.drafts.store(ctx, draft)
const {contentHash} = await db.meta()
drafts.set(mutation.entryId, Promise.resolve({contentHash, draft}))
}

async function parsePreview(
ctx: RequestContext,
preview: PreviewPayload
): Promise<EntryRow | undefined> {
const update = await decodePreviewPayload(preview.payload)
let meta = await db.meta()
if (update.contentHash !== meta.contentHash) {
await syncPending(ctx)
meta = await db.meta()
}
const entry = await resolver.resolve<EntryRow>({
selection: createSelection(
Entry({entryId: update.entryId}).maybeFirst()
),
realm: Realm.PreferDraft
})
if (!entry) return
const cachedDraft = await drafts.get(update.entryId)
let currentDraft: Draft | undefined
if (cachedDraft?.contentHash === meta.contentHash) {
currentDraft = cachedDraft.draft
} else {
try {
const pending = backend.drafts.get(ctx, update.entryId)
drafts.set(
update.entryId,
pending.then(draft => ({contentHash: meta.contentHash, draft}))
)
currentDraft = await pending
} catch (error) {
console.warn('> could not fetch draft', error)
}
}
const apply = currentDraft
? mergeUpdatesV2([currentDraft.draft, update.update])
: update.update
const type = cms.config.schema[entry.type]
if (!type) return
const doc = new Y.Doc()
Y.applyUpdateV2(doc, apply)
const entryData = parseYDoc(type, doc)
return {...entry, ...entryData, path: entry.path}
previews.setDraft(mutation.entryId, {contentHash, draft})
}
})

Expand Down
Loading

0 comments on commit af24254

Please sign in to comment.