import type { Channel, CreateChannelRequest, Session, UpdateChannelRequest } from "@opengeni/sdk"; import { useCallback, useMemo, useSyncExternalStore } from "react"; import { useOpenGeni, type ClientOverride } from "../provider"; import { channelCacheFor } from "./channel-cache"; import { useMutationRunner, usePolledValue } from "./internal"; export type UseChannelsOptions = ClientOverride & { pollIntervalMs?: number | undefined; enabled?: boolean | undefined; }; export type UseChannelsResult = { channels: Channel[]; loading: boolean; error: Error | null; refresh: () => Promise; create: (request: CreateChannelRequest) => Promise; update: (channelId: string, request: UpdateChannelRequest) => Promise; reorder: (channelIds: string[]) => Promise; remove: (channelId: string) => Promise; /** Re-file a session into a channel; null moves it back to the inbox. */ moveSession: (sessionId: string, channelId: string | null) => Promise; mutating: boolean; mutationError: Error | null; clearMutationError: () => void; }; /** * Workspace-shared channels: rail organization for root sessions * ("workstreams") by work type. Pure metadata — filing a session into a * channel never affects execution. */ export function useChannels(options: UseChannelsOptions = {}): UseChannelsResult { const { client, workspaceId } = useOpenGeni(options); const cache = useMemo(() => channelCacheFor(client, workspaceId), [client, workspaceId]); const snapshot = useSyncExternalStore(cache.subscribe, cache.getSnapshot, cache.getSnapshot); const load = useCallback(async () => { const ticket = cache.beginLoad(); const channels = await client.listChannels(workspaceId); cache.acceptLoad(ticket, channels); return channels; }, [cache, client, workspaceId]); const { loading, error, refresh } = usePolledValue(load, { pollIntervalMs: options.pollIntervalMs, enabled: options.enabled, }); const { run, mutating, mutationError, clearMutationError } = useMutationRunner(cache); const create = useCallback( async (request: CreateChannelRequest): Promise => { const result = await run(() => client.createChannel(workspaceId, request)); if (result) { // Publish the canonical write response before this promise can resolve. // The composer may create a session with this id immediately, while a // sibling rail hook could still have an older list request in flight. cache.publishCreated(result); await refresh(); } return result; }, [cache, client, workspaceId, run, refresh], ); const update = useCallback( async (channelId: string, request: UpdateChannelRequest): Promise => { const result = await run(() => client.updateChannel(workspaceId, channelId, request)); if (result) { await refresh(); } return result; }, [client, workspaceId, run, refresh], ); const remove = useCallback( async (channelId: string): Promise => { const result = await run(async () => { await client.deleteChannel(workspaceId, channelId); return true; }); if (result) { await refresh(); } return result === true; }, [client, workspaceId, run, refresh], ); const reorder = useCallback( async (channelIds: string[]): Promise => { const result = await run(() => client.reorderChannels(workspaceId, { channelIds })); if (result) await refresh(); return result; }, [client, workspaceId, run, refresh], ); const moveSession = useCallback( async (sessionId: string, channelId: string | null): Promise => { return await run(() => client.updateSessionChannel(workspaceId, sessionId, { channelId })); }, [client, workspaceId, run], ); return { // Cache snapshots replace arrays instead of mutating them, so preserve the // stable reference expected from the previous polled-value implementation. channels: snapshot.channels as Channel[], loading: loading && !snapshot.ready, error, refresh, create, update, reorder, remove, moveSession, mutating, mutationError, clearMutationError, }; }