Skip to content

Commit

Permalink
feat(core): support remote mutations in version events
Browse files Browse the repository at this point in the history
  • Loading branch information
pedrobonamin committed Nov 21, 2024
1 parent 5182f1c commit 852f7d5
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 19 deletions.
7 changes: 6 additions & 1 deletion packages/sanity/src/core/store/events/getDocumentChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,12 @@ function calculateDiff({
transactions.forEach((transaction, index) => {
const meta: EventMeta = {
transactionIndex: index,
event: events.find((event) => 'revisionId' in event && event.revisionId === transaction.id),
event: events.find(
(event) =>
event.type !== 'EditDocumentVersion' &&
'revisionId' in event &&
event.revisionId === transaction.id,
),
}
const effect = transaction.effects[documentId]
if (effect) {
Expand Down
92 changes: 81 additions & 11 deletions packages/sanity/src/core/store/events/useEventsStore.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import {useCallback, useMemo} from 'react'
import {useObservable} from 'react-rx'
import {combineLatest, from, type Observable, of} from 'rxjs'
import {BehaviorSubject, combineLatest, from, type Observable, of} from 'rxjs'
import {catchError, map, startWith, switchMap, tap} from 'rxjs/operators'
import {
getPublishedId,
getVersionFromId,
isVersionId,
type MendozaPatch,
type SanityClient,
type SanityDocument,
type TransactionLogEventWithEffects,
type WithVersion,
} from 'sanity'

import {useClient} from '../../hooks'
import {useReleasesStore} from '../../releases/store/useReleasesStore'
import {getReleaseIdFromName} from '../../releases/util/getReleaseIdFromName'
import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../studioClient'
import {getPublishedId, getVersionFromId, isVersionId} from '../../util/draftUtils'
import {type DocumentRemoteMutationEvent} from '../_legacy/document/buffered-doc/types'
import {getDocumentChanges} from './getDocumentChanges'
import {getDocumentTransactions} from './getDocumentTransactions'
import {getEditEvents} from './getEditEvents'
Expand All @@ -32,6 +34,7 @@ import {
isUnscheduleDocumentVersionEvent,
isUpdateLiveDocumentEvent,
} from './types'
import {useRemoteMutations} from './useRemoteMutations'

const INITIAL_VALUE = {
events: [],
Expand Down Expand Up @@ -110,16 +113,76 @@ const addEventId = (
}
return {...event, id} as DocumentGroupEvent
}

function remoteMutationToTransaction(
event: DocumentRemoteMutationEvent,
): TransactionLogEventWithEffects {
return {
author: event.author,
documentIDs: [],
id: event.transactionId,
timestamp: event.timestamp.toISOString(),
effects: {
[event.head._id]: {
// TODO: Find a way to validate that is a MendozaPatch
apply: event.effects.apply as MendozaPatch,
revert: event.effects.revert as MendozaPatch,
},
},
}
}

export function useEventsStore({
documentId,
documentType,
rev,
since,
}: {
documentId: string
documentType: string
rev?: string
since?: string
}): EventsStore {
const client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)

const remoteTransactions$ = useMemo(
() => new BehaviorSubject<TransactionLogEventWithEffects[]>([]),
[],
)

const onUpdate = useCallback(
(remoteMutation: WithVersion<DocumentRemoteMutationEvent>) => {
// If the remote mutation happened to a published document we need to re-fetch the events.
// If it happens to a version, we need to add the mutation to the list of events.
// If it happens to a draft: we need to decide if it looks like an event
// Looks like an event: we need to refetch the events list (e.g. publish, discard)
// Doesn't look like an event: we need to add the mutation to the list of events.
const version = remoteMutation.version
if (version === 'version') {
remoteTransactions$.next([
...remoteTransactions$.value,
remoteMutationToTransaction(remoteMutation),
])
return
}
if (version === 'draft') {
console.log('IMPLEMENT ME PLEASE')
return
}
if (version === 'published') {
console.log('IMPLEMENT ME PLEASE')
return
}
console.error('Unknown version', version)
},
[remoteTransactions$],
)
useRemoteMutations({
client,
documentId,
documentType,
onUpdate: onUpdate,
})
const {state$} = useReleasesStore()

const eventsObservable$: Observable<{
Expand All @@ -132,7 +195,7 @@ export function useEventsStore({

const params = new URLSearchParams({
// This is not working yet, CL needs to fix it.
limit: '2',
limit: '50',
})
return client.observable
.request<{
Expand Down Expand Up @@ -164,10 +227,9 @@ export function useEventsStore({
// Get the edit events if necessary.
switchMap((response) => {
if (isPublishedDoc) {
// For the published document we don't need to fetch the edit events.
// For the published document we don't need to fetch the edit transactions.
return of(response)
}
console.log('EVENTS BEFORE EDIT EVENTS', response.events)

// TODO: Improve how we get this value.
const lastVersionRevisionIdIndex = response.events.findIndex(
Expand Down Expand Up @@ -212,10 +274,16 @@ export function useEventsStore({
}, [client, documentId])

const observable$ = useMemo(() => {
return combineLatest([state$, eventsObservable$]).pipe(
map(([releases, {events, nextCursor, loading, error}]) => {
return combineLatest([state$, eventsObservable$, remoteTransactions$]).pipe(
map(([releases, {events, nextCursor, loading, error}, remoteTransactions]) => {
const remoteEdits = getEditEvents(remoteTransactions, documentId)
const withRemoteEdits = [...remoteEdits, ...events].sort(
// Sort by timestamp, newest first
(a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp),
)

return {
events: events.map((event) => {
events: withRemoteEdits.map((event) => {
if (event.type === 'PublishDocumentVersion') {
const releaseId = getVersionFromId(event.versionId)

Expand All @@ -237,7 +305,7 @@ export function useEventsStore({
}
}),
)
}, [state$, eventsObservable$])
}, [state$, eventsObservable$, remoteTransactions$, documentId])

const {events, loading, error, nextCursor} = useObservable(observable$, INITIAL_VALUE)

Expand Down Expand Up @@ -309,6 +377,8 @@ export function useEventsStore({

const changesList = useCallback(
({to, from: fromDoc}: {to: SanityDocument; from: SanityDocument | null}) => {
// TODO: We could pass here the remote edits to avoid fetching them again.
// Consider using the remoteTransactions$ observable to get the remote edits.
return getDocumentChanges({
client,
events,
Expand Down
92 changes: 92 additions & 0 deletions packages/sanity/src/core/store/events/useRemoteMutations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {type SanityClient} from '@sanity/client'
import {useEffect, useMemo, useRef} from 'react'
import {of, type Subscription, switchMap} from 'rxjs'

import {useWorkspace} from '../../studio/workspace'
import {
getDraftId,
getPublishedId,
isDraftId,
isPublishedId,
isVersionId,
} from '../../util/draftUtils'
import {type DocumentRemoteMutationEvent} from '../_legacy/document/buffered-doc/types'
import {remoteSnapshots, type WithVersion} from '../_legacy/document/document-pair'
import {fetchFeatureToggle} from '../_legacy/document/document-pair/utils/fetchFeatureToggle'

/**
* This hooks takes care of listening to the transactions
* received for a single document, it doesn't take the documentPair.
*/
export function useRemoteMutations({
client,
onUpdate,
documentId,
documentType,
}: {
client: SanityClient
documentId: string
documentType: string
onUpdate: (event: WithVersion<DocumentRemoteMutationEvent>) => void
}) {
const snapshotsSubscriptionRef = useRef<Subscription | null>(null)
const workspace = useWorkspace()

const serverActionsEnabled = useMemo(() => {
const configFlag = workspace.__internal_serverDocumentActions?.enabled
// If it's explicitly set, let it override the feature toggle
return typeof configFlag === 'boolean' ? of(configFlag as boolean) : fetchFeatureToggle(client)
}, [client, workspace.__internal_serverDocumentActions?.enabled])

/**
* Fetch document snapshots and update the mutable controller.
* Unsubscribes on clean up, preventing double fetches in strict mode.
*/
useEffect(() => {
if (!snapshotsSubscriptionRef.current) {
snapshotsSubscriptionRef.current = remoteSnapshots(
client,
{
draftId: getDraftId(documentId),
publishedId: getPublishedId(documentId),
...(isVersionId(documentId)
? {
versionId: documentId,
}
: {}),
},
documentType,
serverActionsEnabled,
)
.pipe(
switchMap((event) => {
// Type could be 'snapshot' or 'remoteMutation', we don't want the snapshots
if (event.type !== 'remoteMutation') {
return of(null)
}
// This is to only get the events we're interested in.
// The eventsStore works for only 1 document at a time, not for the docPair.
if (isVersionId(documentId) && event.version === 'version') {
return of(event)
}
if (isPublishedId(documentId) && event.version === 'published') {
return of(event)
}
if (isDraftId(documentId) && event.version === 'draft') {
return of(event)
}
return of(null)
}),
)
.subscribe((ev) => {
if (ev) onUpdate(ev)
})
}
return () => {
if (snapshotsSubscriptionRef.current) {
snapshotsSubscriptionRef.current.unsubscribe()
snapshotsSubscriptionRef.current = null
}
}
}, [client, documentId, documentType, onUpdate, serverActionsEnabled])
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function EventsStoreProvider(props: LegacyStoreProviderProps) {

const eventsStore = useEventsStore({
documentId,
documentType: props.documentType,
rev: params.rev,
since: params.since,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,7 @@ export function Event({event, showChangesBy = 'tooltip'}: TimelineItemProps) {
return formattedDate
}, [timestamp, dateFormat])

const userIds =
// eslint-disable-next-line no-nested-ternary
event.type === 'EditDocumentVersion'
? event.authors
: event.type === 'PublishDocumentVersion'
? [event.publishCause.author]
: [event.author]
const userIds = event.type === 'EditDocumentVersion' ? event.authors : [event.author]

return (
<>
Expand Down

0 comments on commit 852f7d5

Please sign in to comment.