/* eslint-disable @typescript-eslint/no-explicit-any */ import type {SanityDocument} from '@sanity/client' import {getDraftId, getPublishedId} from '@sanity/client/csm' import {createIfNotExists, patch} from '@sanity/mutate' import {isMaybePreviewIframe, isMaybePreviewWindow} from '@sanity/presentation-comlink' import {get as getAtPath} from '@sanity/util/paths' import {useCallback} from 'react' import type { DocumentsGet, DocumentsMutate, OptimisticDocumentPatches, Path, PathValue, } from '../optimistic/types' import {isEmptyActor, type MutatorActor} from '../optimistic/context' import {useOptimisticActor} from './useOptimisticActor' function debounce) => ReturnType>(fn: F, timeout: number): F { let timer: ReturnType return ((...args: Parameters) => { clearTimeout(timer) timer = setTimeout(() => { fn.apply(fn, args) }, timeout) }) as F } function getDocumentsAndSnapshot>(id: string, actor: MutatorActor) { const inFrame = isMaybePreviewIframe() const inPopUp = isMaybePreviewWindow() if (isEmptyActor(actor) || (!inFrame && !inPopUp)) { throw new Error('The `useDocuments` hook cannot be used in this context') } const draftId = getDraftId(id) const publishedId = getPublishedId(id) const documents = actor.getSnapshot().context?.documents const draftDoc = documents?.[draftId] const publishedDoc = documents?.[publishedId] const doc = draftDoc || publishedDoc if (!doc) { throw new Error(`Document "${id}" not found`) } // Helper to get the snapshot from the draft document if it exists, otherwise // fall back to the published document const getDocumentSnapshot = () => (draftDoc.getSnapshot().context?.local || publishedDoc.getSnapshot().context?.local) as | SanityDocument | null | undefined const snapshot = getDocumentSnapshot() const snapshotPromise = new Promise | null>((resolve) => { if (snapshot) { resolve(snapshot) } else { const subscriber = doc.on('ready', (event) => { // Assert type here as the original document mutator machine doesn't // emit a 'ready' event. We provide a custom action to emit it in this // package's internal `createDatasetMutator` function. <3 xstate. const {snapshot} = event as unknown as {snapshot: SanityDocument | null | undefined} resolve(snapshot || null) subscriber.unsubscribe() }) } }) const getSnapshot = () => snapshotPromise return { draftDoc, draftId, getSnapshot, publishedDoc, publishedId, /** * @deprecated - use `getSnapshot` instead */ get snapshot() { // Maintain original error throwing behaviour, to avoid breaking changes if (!snapshot) { throw new Error(`Snapshot for document "${id}" not found`) } return snapshot }, } } function createDocumentCommit>(id: string, actor: MutatorActor) { return (): void => { const {draftDoc} = getDocumentsAndSnapshot(id, actor) draftDoc.send({type: 'submit'}) } } /** * @deprecated - superseded by `createDocumentGetSnapshot` */ function createDocumentGet>(id: string, actor: MutatorActor) { return

>( path?: P, ): PathValue | SanityDocument | undefined => { const {snapshot} = getDocumentsAndSnapshot(id, actor) return path ? (getAtPath(snapshot, path) as PathValue) : (snapshot as unknown as SanityDocument) } } function createDocumentGetSnapshot>( id: string, actor: MutatorActor, ): () => Promise | null> { const {getSnapshot} = getDocumentsAndSnapshot(id, actor) return getSnapshot } function createDocumentPatch>(id: string, actor: MutatorActor) { return async ( patches: OptimisticDocumentPatches, options?: {commit?: boolean | {debounce: number}}, ): Promise => { // Destructure the function result in two steps as we need access to the // `result.snapshot` property in the getter, but don't want to execute the // getter prematurely as it may throw const result = getDocumentsAndSnapshot(id, actor) const {draftDoc, draftId, getSnapshot, publishedId} = result const {commit = true} = options || {} const context = { draftId, publishedId, /** * @deprecated - use `getSnapshot` instead */ get snapshot() { return result.snapshot }, getSnapshot, } const resolvedPatches = await (typeof patches === 'function' ? patches(context) : patches) const _snapshot = await getSnapshot() if (!_snapshot) { throw new Error(`Snapshot for document "${id}" not found`) } draftDoc.send({ type: 'mutate', mutations: [ // Attempt to create the draft document, it might not exist if the // snapshot was from the published document createIfNotExists({..._snapshot, _id: draftId}), // Patch the draft document with the resolved patches patch(draftId, resolvedPatches), ], }) if (commit) { if (typeof commit === 'object' && 'debounce' in commit) { const debouncedCommit = debounce(() => draftDoc.send({type: 'submit'}), commit.debounce) debouncedCommit() } else { draftDoc.send({type: 'submit'}) } } } } export function useDocuments(): { getDocument: DocumentsGet mutateDocument: DocumentsMutate } { const actor = useOptimisticActor() as MutatorActor const getDocument: DocumentsGet = useCallback( >(documentId: string) => { return { id: documentId, commit: createDocumentCommit(documentId, actor), // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - Type instantiation is excessively deep and possibly infinite. get: createDocumentGet(documentId, actor), getSnapshot: createDocumentGetSnapshot(documentId, actor), patch: createDocumentPatch(documentId, actor), } }, [actor], ) const mutateDocument: DocumentsMutate = useCallback( (id, mutations, options) => { const {draftDoc} = getDocumentsAndSnapshot(id, actor) const {commit = true} = options || {} draftDoc.send({ type: 'mutate', mutations: mutations, }) if (commit) { if (typeof commit === 'object' && 'debounce' in commit) { const debouncedCommit = debounce(() => draftDoc.send({type: 'submit'}), commit.debounce) debouncedCommit() } else { draftDoc.send({type: 'submit'}) } } }, [actor], ) return {getDocument, mutateDocument} }