@sanity/preview-kit
Version:
General purpose utils for live content and visual editing
226 lines (225 loc) • 8.31 kB
JavaScript
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