Skip to content

Commit

Permalink
Re-enable live previews
Browse files Browse the repository at this point in the history
  • Loading branch information
benmerckx committed Nov 14, 2023
1 parent 01ee67c commit 3884f28
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 92 deletions.
125 changes: 82 additions & 43 deletions src/backend/Handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@ import {
Config,
Connection,
Draft,
Entry,
EntryPhase,
EntryRow,
PreviewUpdate,
ResolveDefaults,
SyncResponse
SyncResponse,
parseYDoc
} from 'alinea/core'
import {EntryRecord} from 'alinea/core/EntryRecord'
import {EditMutation, Mutation, MutationType} from 'alinea/core/Mutation'
import {Realm} from 'alinea/core/pages/Realm'
import {Selection} from 'alinea/core/pages/Selection'
import {base64} from 'alinea/core/util/Encoding'
import {base64, base64url} from 'alinea/core/util/Encoding'
import {Logger, LoggerResult, Report} from 'alinea/core/util/Logger'
import * as Y from 'alinea/yjs'
import {Type, enums, object, string} from 'cito'
import {unzlibSync} from 'fflate'
import {mergeUpdatesV2} from 'yjs'
import {Database} from './Database.js'
import {DraftTransport, Drafts} from './Drafts.js'
Expand All @@ -31,6 +37,7 @@ export interface HandlerOptions {
config: Config
db: Database
previews: Previews
previewAuthToken: string
auth?: Auth.Server
target?: Target
media?: Media
Expand All @@ -43,46 +50,88 @@ export interface HandlerOptions {
export class Handler {
connect: (ctx: Connection.Context) => Connection
router: Route<Request, Response | undefined>
resolver: EntryResolver
changes: ChangeSetCreator
lastSync = 0

constructor(private options: HandlerOptions) {
const context = {
resolver: new EntryResolver(options.db, options.config.schema),
changes: new ChangeSetCreator(options.config),
...this.options
}
constructor(public options: HandlerOptions) {
this.resolver = new EntryResolver(
options.db,
options.config.schema,
this.parsePreview.bind(this)
)
this.changes = new ChangeSetCreator(options.config)
const auth = options.auth || Auth.anonymous()
this.connect = ctx => new HandlerConnection(context, ctx)
this.connect = ctx => new HandlerConnection(this, ctx)
this.router = createRouter(auth, this.connect)
}
}

interface HandlerContext extends HandlerOptions {
db: Database
resolver: EntryResolver
changes: ChangeSetCreator
previewAuth(): Connection.Context {
return {
logger: new Logger('parsePreview'),
token: this.options.previewAuthToken
}
}

async parsePreview(preview: PreviewUpdate) {
const {config} = this.options
if (Date.now() - this.lastSync > 30_000) await this.syncPending()
const update = unzlibSync(base64url.parse(preview.update))
const entry = await this.resolver.resolve<EntryRow>({
selection: Selection.create(
Entry({entryId: preview.entryId}).maybeFirst()
),
realm: Realm.PreferDraft
})
if (!entry) return
const currentDraft = await this.options.drafts?.getDraft(
preview.entryId,
this.previewAuth()
)
const apply = currentDraft
? mergeUpdatesV2([currentDraft.draft, update])
: update
const type = 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}
}

async syncPending() {
const {pending, db} = this.options
const meta = await db.meta()
if (!pending) return meta
const toApply = await pending.pendingSince(
meta.commitHash,
this.previewAuth()
)
this.lastSync = Date.now()
if (!toApply) return meta
await db.applyMutations(toApply.mutations, toApply.toCommitHash)
return db.meta()
}
}

class HandlerConnection implements Connection {
constructor(
protected handler: HandlerContext,
protected ctx: Connection.Context
) {}
constructor(protected handler: Handler, protected ctx: Connection.Context) {}

// Resolver

resolve = (params: Connection.ResolveParams) => {
const {resolveDefaults} = this.handler
const {resolveDefaults} = this.handler.options
return this.handler.resolver.resolve({...resolveDefaults, ...params})
}

// Target

async mutate(mutations: Array<Mutation>): Promise<{commitHash: string}> {
const {target, media, changes, db} = this.handler
const {target, media, db} = this.handler.options
if (!target) throw new Error('Target not available')
if (!media) throw new Error('Media not available')
const changeSet = changes.create(mutations)
const {commitHash: fromCommitHash} = await this.syncPending()
const changeSet = this.handler.changes.create(mutations)
const {commitHash: fromCommitHash} = await this.handler.syncPending()
const {commitHash: toCommitHash} = await target.mutate(
{commitHash: fromCommitHash, mutations: changeSet},
this.ctx
Expand All @@ -109,7 +158,7 @@ class HandlerConnection implements Connection {
}

previewToken(): Promise<string> {
const {previews} = this.handler
const {previews} = this.handler.options
const user = this.ctx.user
if (!user) return previews.sign({anonymous: true})
return previews.sign({sub: user.sub})
Expand All @@ -118,53 +167,43 @@ class HandlerConnection implements Connection {
// Media

prepareUpload(file: string): Promise<Connection.UploadResponse> {
const {media} = this.handler
const {media} = this.handler.options
if (!media) throw new Error('Media not available')
return media.prepareUpload(file, this.ctx)
}

// History

async revisions(file: string): Promise<Array<Revision>> {
const {history} = this.handler
const {history} = this.handler.options
if (!history) return []
return history.revisions(file, this.ctx)
}

async revisionData(file: string, revisionId: string): Promise<EntryRecord> {
const {history} = this.handler
const {history} = this.handler.options
if (!history) throw new Error('History not available')
return history.revisionData(file, revisionId, this.ctx)
}

// Syncable

private async syncPending() {
const {pending, db} = this.handler
const meta = await db.meta()
if (!pending) return meta
const toApply = await pending.pendingSince(meta.commitHash, this.ctx)
if (!toApply) return meta
await db.applyMutations(toApply.mutations, toApply.toCommitHash)
return db.meta()
}

async syncRequired(contentHash: string): Promise<boolean> {
const {db} = this.handler
await this.syncPending()
const {db} = this.handler.options
await this.handler.syncPending()
return db.syncRequired(contentHash)
}

async sync(contentHashes: Array<string>): Promise<SyncResponse> {
const {db} = this.handler
await this.syncPending()
const {db} = this.handler.options
await this.handler.syncPending()
return db.sync(contentHashes)
}

// Drafts

private async persistEdit(mutation: EditMutation) {
const {drafts} = this.handler
const {drafts} = this.handler.options
if (!drafts || !mutation.update) return
const update = base64.parse(mutation.update)
const currentDraft = await this.getDraft(mutation.entryId)
Expand All @@ -178,13 +217,13 @@ class HandlerConnection implements Connection {
}

getDraft(entryId: string): Promise<Draft | undefined> {
const {drafts} = this.handler
const {drafts} = this.handler.options
if (!drafts) throw new Error('Drafts not available')
return drafts.getDraft(entryId, this.ctx)
}

storeDraft(draft: Draft): Promise<void> {
const {drafts} = this.handler
const {drafts} = this.handler.options
if (!drafts) throw new Error('Drafts not available')
return drafts.storeDraft(draft, this.ctx)
}
Expand Down
31 changes: 11 additions & 20 deletions src/backend/resolver/EntryResolver.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import {
Connection,
Field,
PreviewUpdate,
ResolveDefaults,
Schema,
Type,
createYDoc,
parseYDoc,
unreachable
} from 'alinea/core'
import {EntrySearch} from 'alinea/core/EntrySearch'
import {Realm} from 'alinea/core/pages/Realm'
import {base64url} from 'alinea/core/util/Encoding'
import {entries, fromEntries, keys} from 'alinea/core/util/Objects'
import * as Y from 'alinea/yjs'
import {unzlibSync} from 'fflate'
import {
BinOpType,
Expr,
Expand Down Expand Up @@ -169,6 +165,9 @@ export class EntryResolver {
constructor(
public db: Database,
public schema: Schema,
public parsePreview?: (
preview: PreviewUpdate
) => Promise<EntryRow | undefined>,
public defaults?: ResolveDefaults
) {
this.targets = Schema.targets(schema)
Expand Down Expand Up @@ -702,25 +701,17 @@ export class EntryResolver {
const queryData = this.query(ctx, selection)
const query = new Query<Interim>(queryData)
if (preview) {
const current = EntryRow({
entryId: preview.entryId,
active: true
})
const entry = await this.db.store(current.maybeFirst())
if (entry)
const updated = await this.parsePreview?.(preview)
if (updated)
try {
// Create yjs doc
const type = this.schema[entry.type]
const yDoc = createYDoc(type, entry)
// Apply update
const update = unzlibSync(base64url.parse(preview.update))
Y.applyUpdateV2(yDoc, update)
const entryData = parseYDoc(type, yDoc)
const previewEntry = {...entry, ...entryData}
await this.db.store.transaction(async tx => {
const current = EntryRow({
entryId: preview.entryId,
active: true
})
// Temporarily add preview entry
await tx(current.delete())
await tx(EntryRow().insert(previewEntry))
await tx(EntryRow().insert(updated))
await Database.index(tx)
const result = await tx(query)
const linkResolver = new LinkResolver(this, tx, ctx.realm)
Expand Down
3 changes: 2 additions & 1 deletion src/cli/Serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ export async function serve(options: ServeOptions): Promise<void> {
media: fileData,
drafts,
history: new GitHistory(currentCMS, rootDir),
previews: new JWTPreviews('dev')
previews: new JWTPreviews('dev'),
previewAuthToken: 'dev'
})
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/cloud/server/CloudDebugHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {Config, Connection, Draft, createId} from 'alinea/core'
import {EntryRecord} from 'alinea/core/EntryRecord'
import {Mutation} from 'alinea/core/Mutation'

const latency = 2000
const latency = 0

const lag = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))

Expand Down Expand Up @@ -93,6 +93,7 @@ export function createCloudDebugHandler(config: Config, db: Database) {
history: api,
drafts: api,
pending: api,
previews: new JWTPreviews('dev')
previews: new JWTPreviews('dev'),
previewAuthToken: 'dev'
})
}
3 changes: 2 additions & 1 deletion src/cloud/server/CloudHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ export function createCloudHandler(
history: api,
pending: api,
drafts: api,
previews: new JWTPreviews(apiKey!)
previews: new JWTPreviews(apiKey!),
previewAuthToken: apiKey!
})
}
14 changes: 5 additions & 9 deletions src/core/driver/NextDriver.server.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {JWTPreviews} from 'alinea/backend'
import {EntryResolver} from 'alinea/backend/resolver/EntryResolver'
import {createCloudHandler} from 'alinea/cloud/server/CloudHandler'
import {parseChunkedCookies} from 'alinea/preview/ChunkCookieValue'
import {
Expand Down Expand Up @@ -59,22 +58,19 @@ class NextDriver extends DefaultDriver implements NextApi {
url: devUrl,
resolveDefaults
})
const db = await this.db
return new EntryResolver(db, this.config.schema, resolveDefaults)
const handler = await this.cloudHandler
return handler.resolver
}

backendHandler = async (request: Request) => {
const handler = await this.cloudHandler
return handler(request)
const response = await handler.router.handle(request)
return response ?? new Response('Not found', {status: 404})
}

cloudHandler = PLazy.from(async () => {
const db = await this.db
const handler = createCloudHandler(this, db, this.apiKey)
return async (request: Request) => {
const response = await handler.router.handle(request)
return response ?? new Response('Not found', {status: 404})
}
return createCloudHandler(this, db, this.apiKey)
})

previewHandler = async (request: Request) => {
Expand Down
3 changes: 2 additions & 1 deletion src/core/driver/TestDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ class TestDriver extends DefaultDriver implements TestApi {
const handler = new Handler({
config: this,
db,
previews: new JWTPreviews('test')
previews: new JWTPreviews('test'),
previewAuthToken: 'test'
})
return handler.connect({logger: new Logger('test')})
})
Expand Down
3 changes: 3 additions & 0 deletions src/dashboard/atoms/Edits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export class Edits {
isLoading = yAtom(this.root, () => {
return !this.hasData()
})
yUpdate = yAtom(this.root, () => {
return this.getLocalUpdate()
})

constructor(private entryId: string) {}

Expand Down
Loading

0 comments on commit 3884f28

Please sign in to comment.