Skip to main content
Glama

Storyden

by Southclaws
Mozilla Public License 2.0
229
Context.tsx4.94 kB
import { dequal } from "dequal"; import { debounce, entries, toPairs } from "lodash"; import { PropsWithChildren, createContext, useContext, useEffect, useMemo, useRef, useState, } from "react"; import { Identifier, NodeListResult, NodeWithChildren, } from "src/api/openapi-schema"; import { nodeUpdate, nodeUpdateChildrenPropertySchema, } from "@/api/openapi-client/nodes"; import { MutationSet } from "@/lib/library/diff"; import { useLibraryMutation } from "@/lib/library/library"; import { WithMetadata, hydrateNode } from "@/lib/library/metadata"; import { deriveError } from "@/utils/error"; import { NodeStoreAPI, createNodeStore } from "./store"; type LibraryPageContext = { nodeID: Identifier; initialNode: WithMetadata<NodeWithChildren>; initialChildren?: NodeListResult; store: NodeStoreAPI; saving: boolean; }; const Context = createContext<LibraryPageContext | null>(null); export function useLibraryPageContext() { const context = useContext(Context); if (!context) { throw new Error( "useLibraryPageContext must be used within a LibraryPageProvider", ); } return context; } export type Props = { node: NodeWithChildren; childNodes?: NodeListResult; }; export function LibraryPageProvider({ node, childNodes, children, }: PropsWithChildren<Props>) { const [saving, setSaving] = useState(false); const nodeWithMeta = useMemo(() => hydrateNode(node), [node]); const { revalidate } = useLibraryMutation(node); const storeRef = useRef<NodeStoreAPI | null>(null); if (storeRef.current === null) { storeRef.current = createNodeStore({ original: nodeWithMeta, draft: nodeWithMeta, }); } const saveDraft = useRef( debounce(() => { if (!storeRef.current) { return; } const state = storeRef.current.getState(); state.commit(async (mutation: MutationSet) => { try { setSaving(() => true); if (mutation.childPropertySchemaMutation) { await nodeUpdateChildrenPropertySchema( node.id, mutation.childPropertySchemaMutation, ); } if (mutation.childMutation) { const collapsed = Object.fromEntries( Object.entries(mutation.childMutation).map(([id, changes]) => [ id, changes.at(-1)!, ]), ); const operations = entries(collapsed); console.log("Updating child nodes", operations); await Promise.all( operations.map(([childNodeID, child]) => nodeUpdate(childNodeID, child), ), ); } const updated = await nodeUpdate(node.id, mutation.nodeMutation); await revalidate(updated); const slugChanged = updated.slug !== state.original.slug; if (slugChanged) { window.history.replaceState( null, "", `/l/${updated.slug}?edit=true`, ); } return updated; } catch (error) { throw new Error(deriveError(error), { cause: error }); } finally { setTimeout(() => { setSaving(() => false); }, 500); } }); }, 500), ).current; useEffect(() => { if (!storeRef.current) { return; } const unsub = storeRef.current.subscribe((state, prev) => { if (!dequal(state.draft, prev.draft)) { saveDraft(); } }); return unsub; }, [saveDraft]); // Cancel the saveDraft debounce when the component unmounts. useEffect(() => { return () => { saveDraft.cancel(); }; }, []); // Handle external changes to the original node state. This happens if another // source triggers a mutation+revalidation via SWR and the initial must update // the store state. This hook must run after the store subscription is set up. useEffect(() => { if (!storeRef.current) { return; } const { original, draft } = storeRef.current.getState(); // We compare the un-hydrated node for original comparison, because the // nodeWithMeta object is potentially mutated by the hydration function to // set up default values for new nodes. This includes the page's layout. const equalToOriginal = dequal(original, node); const equalToDraft = dequal(draft, nodeWithMeta); storeRef.current.setState((state) => { if (!equalToOriginal) { state.original = node; } if (!equalToDraft) { state.draft = nodeWithMeta; } }); }, [node, nodeWithMeta]); return ( <Context.Provider value={{ nodeID: node.id, initialNode: nodeWithMeta, initialChildren: childNodes, store: storeRef.current, saving, }} > {children} </Context.Provider> ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Southclaws/storyden'

If you have feedback or need assistance with the MCP directory API, please join our Discord server