UNPKG

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

174 lines (158 loc) • 5.09 kB
/* eslint-disable max-nested-callbacks */ import {type SanityClient} from '@sanity/client' import {type Schema} from '@sanity/types' import {asyncScheduler, defer, EMPTY, merge, type Observable, of, Subject} from 'rxjs' import { catchError, filter, groupBy, last, map, mergeMap, mergeMapTo, share, switchMap, take, tap, throttleTime, } from 'rxjs/operators' import {type HistoryStore} from '../../history' import {type IdPair} from '../types' import {memoize} from '../utils/createMemoizer' import {consistencyStatus} from './consistencyStatus' import {operationArgs} from './operationArgs' import {type OperationArgs, type OperationsAPI} from './operations' import {commit} from './operations/commit' import {del} from './operations/delete' import {discardChanges} from './operations/discardChanges' import {duplicate} from './operations/duplicate' import {patch} from './operations/patch' import {publish} from './operations/publish' import {restore} from './operations/restore' import {unpublish} from './operations/unpublish' interface ExecuteArgs { operationName: keyof OperationsAPI idPair: IdPair typeName: string extraArgs: any[] } function maybeObservable(v: void | Observable<any>) { return typeof v === 'undefined' ? of(null) : v } const operationImpls = { del: del, delete: del, publish, patch, commit, discardChanges, unpublish, duplicate, restore, } as const const execute = ( operationName: keyof typeof operationImpls, operationArguments: OperationArgs, extraArgs: any[], ): Observable<any> => { const operation = operationImpls[operationName] return defer(() => merge(of(null), maybeObservable(operation.execute(operationArguments, ...extraArgs))), ).pipe(last()) } const operationCalls$ = new Subject<ExecuteArgs>() /** @internal */ export function emitOperation( operationName: keyof OperationsAPI, idPair: IdPair, typeName: string, extraArgs: any[], ): void { operationCalls$.next({operationName, idPair, typeName, extraArgs}) } // These are the operations that cannot be performed while the document is in an inconsistent state const REQUIRES_CONSISTENCY = ['publish', 'unpublish', 'discardChanges', 'delete'] /** * @hidden * @beta */ export interface OperationError { type: 'error' /** @internal */ op: keyof OperationsAPI id: string error: Error } /** * @hidden * @beta */ export interface OperationSuccess { type: 'success' /** @internal */ op: keyof OperationsAPI id: string } interface IntermediarySuccess { type: 'success' args: ExecuteArgs } interface IntermediaryError { type: 'error' args: ExecuteArgs error: any } /** @internal */ export const operationEvents = memoize( (ctx: {client: SanityClient; historyStore: HistoryStore; schema: Schema}) => { const result$: Observable<IntermediarySuccess | IntermediaryError> = operationCalls$.pipe( groupBy((op) => op.idPair.publishedId), mergeMap((groups$) => groups$.pipe( // although it might look like a bug, dropping pending async operations here is actually a feature // E.g. if the user types `publish` which is async and then starts patching (sync) then the publish // should be cancelled switchMap((args) => operationArgs(ctx, args.idPair, args.typeName).pipe( take(1), switchMap((operationArguments) => { const requiresConsistency = REQUIRES_CONSISTENCY.includes(args.operationName) if (requiresConsistency) { operationArguments.published.commit() operationArguments.draft.commit() } const isConsistent$ = consistencyStatus( ctx.client, args.idPair, args.typeName, ).pipe(filter(Boolean)) const ready$ = requiresConsistency ? isConsistent$.pipe(take(1)) : of(true) return ready$.pipe( switchMap(() => execute(args.operationName, operationArguments, args.extraArgs)), ) }), map((): IntermediarySuccess => ({type: 'success', args})), catchError( (err): Observable<IntermediaryError> => of({type: 'error', args, error: err}), ), ), ), ), ), share(), ) // this enables autocommit after patch operations const AUTOCOMMIT_INTERVAL = 1000 const autoCommit$ = result$.pipe( filter((result) => result.type === 'success' && result.args.operationName === 'patch'), throttleTime(AUTOCOMMIT_INTERVAL, asyncScheduler, {leading: true, trailing: true}), tap((result) => { emitOperation('commit', result.args.idPair, result.args.typeName, []) }), ) return merge(result$, autoCommit$.pipe(mergeMapTo(EMPTY))) }, (ctx) => { const config = ctx.client.config() // we only want one of these per dataset+projectid return `${config.dataset ?? ''}-${config.projectId ?? ''}` }, )