sanity
Version:
Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches
156 lines (135 loc) • 4.81 kB
text/typescript
import {type SanityClient} from '@sanity/client'
import {type Mutation} from '@sanity/mutator'
import {type SanityDocument} from '@sanity/types'
import {EMPTY, from, merge, type Observable, Subject} from 'rxjs'
import {filter, map, mergeMap, mergeMapTo, share, tap} from 'rxjs/operators'
import {
type BufferedDocumentEvent,
type CommitRequest,
createBufferedDocument,
type MutationPayload,
type RemoteSnapshotEvent,
} from '../buffered-doc'
import {getPairListener, type ListenerEvent} from '../getPairListener'
import {type IdPair, type PendingMutationsEvent, type ReconnectEvent} from '../types'
const isMutationEventForDocId =
(id: string) =>
(
event: ListenerEvent,
): event is Exclude<ListenerEvent, ReconnectEvent | PendingMutationsEvent> => {
return event.type !== 'reconnect' && event.type !== 'pending' && event.documentId === id
}
/**
* @hidden
* @beta */
export type WithVersion<T> = T & {version: 'published' | 'draft'}
/**
* @hidden
* @beta */
export type DocumentVersionEvent = WithVersion<ReconnectEvent | BufferedDocumentEvent>
/**
* @hidden
* @beta */
export type RemoteSnapshotVersionEvent = WithVersion<RemoteSnapshotEvent>
/**
* @hidden
* @beta */
export interface DocumentVersion {
consistency$: Observable<boolean>
remoteSnapshot$: Observable<RemoteSnapshotVersionEvent>
events: Observable<DocumentVersionEvent>
patch: (patches: any[]) => MutationPayload[]
create: (document: Partial<SanityDocument>) => MutationPayload
createIfNotExists: (document: SanityDocument) => MutationPayload
createOrReplace: (document: SanityDocument) => MutationPayload
delete: () => MutationPayload
mutate: (mutations: MutationPayload[]) => void
commit: () => void
}
/**
* @hidden
* @beta */
export interface Pair {
/** @internal */
transactionsPendingEvents$: Observable<PendingMutationsEvent>
published: DocumentVersion
draft: DocumentVersion
complete: () => void
}
function setVersion<T>(version: 'draft' | 'published') {
return (ev: T): T & {version: 'draft' | 'published'} => ({...ev, version})
}
function commitMutations(client: SanityClient, mutationParams: Mutation['params']) {
const {resultRev, ...mutation} = mutationParams
return client.dataRequest('mutate', mutation, {
visibility: 'async',
returnDocuments: false,
tag: 'document.commit',
// This makes sure the studio doesn't crash when a draft is crated
// because someone deleted a referenced document in the target dataset
skipCrossDatasetReferenceValidation: true,
})
}
function submitCommitRequest(client: SanityClient, request: CommitRequest) {
return from(commitMutations(client, request.mutation.params)).pipe(
tap({
error: (error) => {
const isBadRequest =
'statusCode' in error &&
typeof error.statusCode === 'number' &&
error.statusCode >= 400 &&
error.statusCode <= 500
if (isBadRequest) {
request.cancel(error)
} else {
request.failure(error)
}
},
next: () => request.success(),
}),
)
}
/** @internal */
export function checkoutPair(client: SanityClient, idPair: IdPair): Pair {
const {publishedId, draftId} = idPair
const listenerEventsConnector = new Subject<ListenerEvent>()
const listenerEvents$ = getPairListener(client, idPair).pipe(
share({connector: () => listenerEventsConnector}),
)
const reconnect$ = listenerEvents$.pipe(
filter((ev) => ev.type === 'reconnect'),
) as Observable<ReconnectEvent>
const draft = createBufferedDocument(
draftId,
listenerEvents$.pipe(filter(isMutationEventForDocId(draftId))),
)
const published = createBufferedDocument(
publishedId,
listenerEvents$.pipe(filter(isMutationEventForDocId(publishedId))),
)
// share commit handling between draft and published
const transactionsPendingEvents$ = listenerEvents$.pipe(
filter((ev): ev is PendingMutationsEvent => ev.type === 'pending'),
)
const commits$ = merge(draft.commitRequest$, published.commitRequest$).pipe(
mergeMap((commitRequest) => submitCommitRequest(client, commitRequest)),
mergeMapTo(EMPTY),
share(),
)
return {
transactionsPendingEvents$,
draft: {
...draft,
events: merge(commits$, reconnect$, draft.events).pipe(map(setVersion('draft'))),
consistency$: draft.consistency$,
remoteSnapshot$: draft.remoteSnapshot$.pipe(map(setVersion('draft'))),
},
published: {
...published,
events: merge(commits$, reconnect$, published.events).pipe(map(setVersion('published'))),
consistency$: published.consistency$,
remoteSnapshot$: published.remoteSnapshot$.pipe(map(setVersion('published'))),
},
complete: () => listenerEventsConnector.complete(),
}
}