// Browser-side text-post publishing. The composer text is only cleared after // the new post id shows up on the reloaded first page of the home feed. import { fetchFeedPage, FeedLoadError, type FeedPage } from "./feed-client"; export class PostError extends Error { constructor(public readonly signedOut: boolean) { super(signedOut ? "sign_in" : "publish_failed"); } } export async function sendPost(content: string, fetchImpl: typeof fetch = fetch): Promise { let res: Response; try { res = await fetchImpl("/api/posts", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content }), }); } catch { throw new PostError(false); } if (res.status === 401) throw new PostError(true); if (!res.ok) throw new PostError(false); try { const b = (await res.json()) as { id?: unknown }; if ((typeof b.id === "string" && b.id) || typeof b.id === "number") return String(b.id); } catch { /* fall through */ } throw new PostError(false); } function pageHasPost(page: FeedPage, id: string): boolean { return page.items.some((item) => { const post = (item as { post?: { id?: unknown } } | null)?.post; return post != null && String(post.id) === id; }); } /** * Reload the first page. Returns the page when it holds the post id, else null. * Throws PostError when the person is signed out or the reload fails. */ export async function confirmOnFirstPage( id: string, fetchPage: (cursor: string | null) => Promise = fetchFeedPage, ): Promise { try { const page = await fetchPage(null); return pageHasPost(page, id) ? page : null; } catch (e) { throw new PostError(e instanceof FeedLoadError && e.signedOut); } } export interface PublishDeps { send?: (content: string) => Promise; fetchPage?: (cursor: string | null) => Promise; } /** * Publish text and confirm it on the reloaded first page. Returns that page * only when the post id is on it. `unconfirmed` holds an id that was created * earlier but not yet seen, so a retry re-checks the feed and does not create * a duplicate. Throws PostError otherwise; the caller keeps the composer text. */ export async function publishAndConfirm( content: string, unconfirmed: { id: string | null }, deps: PublishDeps = {}, ): Promise { const id = unconfirmed.id ?? (await (deps.send ?? sendPost)(content)); unconfirmed.id = id; const page = await confirmOnFirstPage(id, deps.fetchPage); if (!page) throw new PostError(false); unconfirmed.id = null; return page; }