UNPKG

@sanity/preview-kit

Version:

General purpose utils for live content and visual editing

226 lines (225 loc) 8.31 kB
import { jsxs, jsx } from "react/jsx-runtime"; import { useReducer, useState, useDeferredValue, useEffect, useCallback, useMemo } from "react"; import { isEqual, defineStoreContext, useShouldPause } from "./hooks.js"; import { createNode, createNodeMachine } from "@sanity/comlink"; import { createCompatibilityActors } from "@sanity/presentation-comlink"; const DEFAULT_TAG = "sanity.preview-kit"; function reducer$1(state, event) { switch (event.type) { case "message": return { ...state, messages: [...state.messages, event] }; case "reconnect": case "restart": return { ...state, messages: [], resets: state.resets + 1 }; case "welcome": return state; default: throw Error( `Unknown event: ${// eslint-disable-next-line @typescript-eslint/no-explicit-any event.type}`, { cause: event } ); } } const initialState = { messages: [], resets: 0 }; function useLiveEvents(client) { const [state, dispatch] = useReducer(reducer$1, initialState), [error, setError] = useState(null); if (error !== null) throw error; return useEffect(() => { const subscription = client.live.events({ includeDrafts: !0, tag: DEFAULT_TAG }).subscribe({ next: dispatch, error: (err) => setError( err instanceof Error ? err : new Error("Unexpected error in useLiveEvents", { cause: err }) ) }); return () => subscription.unsubscribe(); }, [client.live]), useDeferredValue(state); } function getQueryCacheKey(query, params) { return `${query}:${JSON.stringify(params)}`; } function subscribe(queries, { payload }) { const key = getQueryCacheKey(payload.query, payload.params); if (!queries.get(key)?.listeners.has(payload.onStoreChange)) { const nextQueries = new Map(queries), value = nextQueries.get(key) || { query: payload.query, params: payload.params, listeners: /* @__PURE__ */ new Set() }, listeners = new Set(value.listeners); return listeners.add(payload.onStoreChange), nextQueries.set(key, { ...value, listeners }), nextQueries; } return queries; } function unsubscribe(queries, { payload }) { const key = getQueryCacheKey(payload.query, payload.params), value = queries.get(key); if (!value || !value.listeners.has(payload.onStoreChange)) return queries; const nextQueries = new Map(queries), listeners = new Set(value.listeners); return listeners.delete(payload.onStoreChange), listeners.size === 0 ? nextQueries.delete(key) : nextQueries.set(key, { ...value, listeners }), nextQueries; } function reducer(state, action) { switch (action.type) { case "subscribe": return subscribe(state, action); case "unsubscribe": return unsubscribe(state, action); default: throw Error( `Unknown action: ${// eslint-disable-next-line @typescript-eslint/no-explicit-any action.type}`, { cause: action } ); } } const initialQueries = /* @__PURE__ */ new Map(); function useLiveQueries() { const [queries, dispatch] = useReducer(reducer, initialQueries), [snapshots] = useState(() => /* @__PURE__ */ new Map()), subscribe2 = useCallback((payload) => (dispatch({ type: "subscribe", payload }), () => dispatch({ type: "unsubscribe", payload })), []), update = useCallback( (key, result, resultSourceMap, syncTags) => { const prev = snapshots.get(key); return prev && isEqual(prev, { result, resultSourceMap, syncTags }) ? !1 : (snapshots.set(key, { result: isEqual(prev?.result, result) ? prev?.result : result, resultSourceMap: isEqual(prev?.resultSourceMap, resultSourceMap) ? prev?.resultSourceMap : resultSourceMap, syncTags: isEqual(prev?.syncTags, syncTags) ? prev?.syncTags : syncTags }), !0); }, [snapshots] ); return { queries, snapshots, subscribe: subscribe2, update }; } function usePerspective(initialPerspective) { const [presentationPerspective, setPresentationPerspective] = useState(null); return useEffect(() => { const comlink = createNode( { name: "loaders", connectTo: "presentation" }, createNodeMachine().provide({ actors: createCompatibilityActors() }) ); comlink.on("loader/perspective", ({ perspective }) => { perspective !== "raw" && setPresentationPerspective((prev) => isEqual(prev, perspective) ? prev : perspective); }); const stop = comlink.start(); return () => stop(); }, []), presentationPerspective === null ? initialPerspective : presentationPerspective; } function LiveStoreProvider(props) { const { children, token } = props; if (!props.client) throw new Error("Missing a `client` prop with a configured Sanity client instance"); const perspective = usePerspective(props.perspective || "drafts"), [client] = useState(() => { const { requestTagPrefix } = props.client.config(); return props.client.withConfig({ requestTagPrefix: requestTagPrefix || DEFAULT_TAG, // Set the recommended defaults, this is a convenience to make it easier to share a client config from a server component to the client component ...token && { token, useCdn: !1, perspective: "drafts", ignoreBrowserTokenWarning: !0 } }); }), [logger] = useState(() => props.logger); useEffect(() => { logger && logger.log( "[@sanity/preview-kit]: Updates will be applied in real-time using the Sanity Live Content API." ); }, [logger]); const { queries, snapshots, subscribe: subscribe2, update } = useLiveQueries(), context = useMemo(() => function(initialSnapshot, query, params) { const snapshotsKey = getQueryCacheKey(query, params); return { subscribe: (onStoreChange) => { const unsubscribe2 = subscribe2({ query, params, onStoreChange }); return () => unsubscribe2(); }, getSnapshot: () => snapshots.has(snapshotsKey) ? snapshots.get(snapshotsKey)?.result : initialSnapshot }; }, [snapshots, subscribe2]), liveEvents = useLiveEvents(client); return /* @__PURE__ */ jsxs(defineStoreContext.Provider, { value: context, children: [ children, [...queries.entries()].map(([key, { query, params, listeners }]) => /* @__PURE__ */ jsx( QuerySubscription, { client, listeners, params, query, perspective, liveEventsMessages: liveEvents.messages, snapshotKey: key, syncTags: snapshots.get(key)?.syncTags, update }, `${liveEvents.resets}:${perspective}:${key}` )) ] }); } LiveStoreProvider.displayName = "LiveStoreProvider"; function QuerySubscription(props) { const { client, query, params, perspective, snapshotKey, update, liveEventsMessages, syncTags, listeners } = props, [skipEventIds] = useState(() => new Set(liveEventsMessages.map((msg) => msg.id))), recentLiveEvents = useMemo( () => liveEventsMessages.filter((msg) => !skipEventIds.has(msg.id)), [liveEventsMessages, skipEventIds] ), lastLiveEventId = useMemo( () => recentLiveEvents.findLast((msg) => msg.tags.some((tag) => syncTags?.includes(tag))), [recentLiveEvents, syncTags] )?.id, [error, setError] = useState(null); if (error) throw error; const shouldPause = useShouldPause(); return useEffect(() => { if (shouldPause) return; let fulfilled = !1; const controller = new AbortController(); return client.fetch(query, params, { lastLiveEventId, perspective, signal: controller.signal, filterResponse: !1, returnQuery: !1 }).then(({ result, resultSourceMap, syncTags: nextTags }) => { update(snapshotKey, result, resultSourceMap, nextTags); for (const listener of listeners) listener(); fulfilled = !0; }).catch((error2) => { error2.name !== "AbortError" && setError(error2); }), () => { fulfilled || controller.abort(); }; }, [ client, lastLiveEventId, listeners, params, perspective, query, shouldPause, snapshotKey, update ]), null; } QuerySubscription.displayName = "QuerySubscription"; export { LiveStoreProvider as default }; //# sourceMappingURL=LiveQueryProvider.js.map