import { AnyDocumentId, DocHandle } from "@automerge/automerge-repo/slim" import { PromiseWrapper, wrapPromise } from "./wrapPromise.js" import { useRepo } from "./useRepo.js" import { useEffect, useRef, useState } from "react" import { anyDocumentIdToAutomergeUrl } from "../../automerge-repo/dist/AutomergeUrl.js" // Shared with useDocHandles export const wrapperCache = new Map< AnyDocumentId, PromiseWrapper> >() // NB: this is a global cache that isn't keyed on the Repo // so if your app uses the same documents in two Repos // this could cause problems. please let me know if you do. interface UseDocHandleSuspendingParams { suspense: true } interface UseDocHandleSynchronousParams { suspense: false } type UseDocHandleParams = | UseDocHandleSuspendingParams | UseDocHandleSynchronousParams export function useDocHandle( id: AnyDocumentId, params: UseDocHandleSuspendingParams ): DocHandle export function useDocHandle( id: AnyDocumentId | undefined, params?: UseDocHandleSynchronousParams ): DocHandle | undefined export function useDocHandle( id: AnyDocumentId | undefined, { suspense }: UseDocHandleParams = { suspense: false } ): DocHandle | undefined { const repo = useRepo() const controllerRef = useRef() const [handle, setHandle] = useState | undefined>() let currentHandle: DocHandle | undefined = // make sure the handle matches the id id && handle && handle.url === anyDocumentIdToAutomergeUrl(id) ? handle : undefined if (id && !currentHandle) { // if we haven't saved a handle yet, check if one is immediately available const progress = repo.findWithProgress(id) if (progress.state === "ready") { currentHandle = progress.handle } } let wrapper = id ? wrapperCache.get(id) : undefined if (!wrapper && id) { controllerRef.current?.abort() controllerRef.current = new AbortController() const promise = repo.find(id, { signal: controllerRef.current.signal }) wrapper = wrapPromise(promise) wrapperCache.set(id, wrapper) } /* From here we split into two paths: suspense and not. * In the suspense path, we return the wrapper directly. * In the non-suspense path, we wait for the promise to resolve * and then set the handle via setState. Suspense relies on * re-running this function until it succeeds, whereas the synchronous * form uses a setState to track the value. */ useEffect(() => { if (currentHandle || suspense || !wrapper) { return } wrapper.promise .then(handle => { setHandle(handle as DocHandle) }) .catch(() => { setHandle(undefined) }) }, [currentHandle, suspense, wrapper]) if (currentHandle || !suspense || !wrapper) { return currentHandle } return wrapper.read() as DocHandle }