import { organization, type OrganizationBase, type SanityInstance, } from "@sanity/sdk"; import { distinctUntilChanged, filter, map } from "rxjs"; import { assign, fromObservable, setup } from "xstate"; import { os } from "../runtime/bus"; import type { OSBaseInput } from "./root.machine"; /** * The id comes from the OS config, not from the session. * @internal */ export interface OrganizationInput extends OSBaseInput { organizationId: string; } /** * The resolved organization the parent keys its resource queries on. Exported so * the root machine can read it off this actor's snapshot. * @internal */ export type CurrentOrganization = Pick< OrganizationBase, "id" | "name" | "slug" >; const sameOrganization = ( a: CurrentOrganization, b: CurrentOrganization, ): boolean => a.id === b.id && a.name === b.name && a.slug === b.slug; // Stays subscribed for as long as the session is open: the store keeps emitting // after the first value — revalidation, an explicit invalidation, a switch — and // reading it once would strand `organizations.current` on the boot-time value. const organizationStateLogic = fromObservable< CurrentOrganization, OrganizationInput >(({ input }) => organization .getState(input.instance, { organizationId: input.organizationId }) .observable.pipe( // Fetcher snapshots carry their status rather than throwing: there is // nothing to publish while pending, and a failed fetch has to break the // stream for `onError` to fail the fetch. map((snapshot) => { if (snapshot.status === "error") throw snapshot.error; if (snapshot.status !== "success") return null; const { id, name, slug } = snapshot.data; return { id, name, slug }; }), filter((org): org is CurrentOrganization => org !== null), // The bus compares published values by reference, so a revalidation that // returned identical data would otherwise wake every subscriber. distinctUntilChanged(sameOrganization), ), ); type OrganizationContext = { instance: SanityInstance; organizationId: string; /** * The resolved organization, or `null` until the first fetch settles. Held in * context so the parent can read the resolved `id` off this actor's snapshot. */ currentOrganization: CurrentOrganization | null; }; export const organizationLogic = setup({ types: { input: {} as OrganizationInput, context: {} as OrganizationContext, tags: {} as "organization-resolved" | "error", }, actors: { organizationState: organizationStateLogic }, actions: { publishOrganization: ( _, params: { organization: CurrentOrganization | null }, ) => { os.emit("organizations.current", params.organization); }, storeOrganization: assign({ currentOrganization: ( _, params: { organization: CurrentOrganization | null }, ) => params.organization, }), }, }).createMachine({ id: "organization", context: ({ input }) => ({ instance: input.instance, organizationId: input.organizationId, currentOrganization: null, }), invoke: { src: "organizationState", input: ({ context }) => ({ instance: context.instance, organizationId: context.organizationId, }), onSnapshot: { // `resolved` has no entry actions, so the re-entry each later value causes // is free. guard: ({ event }) => Boolean(event.snapshot.context), target: ".resolved", actions: [ { type: "storeOrganization", params: ({ event }) => ({ organization: event.snapshot.context ?? null, }), }, { type: "publishOrganization", params: ({ event }) => ({ organization: event.snapshot.context ?? null, }), }, ], }, onError: { target: ".failed" }, }, initial: "resolving", states: { resolving: {}, resolved: { tags: ["organization-resolved"] }, failed: { tags: ["error"], // Clears the last good value, so consumers stop reading an organization // that can no longer be resolved. entry: [{ type: "publishOrganization", params: { organization: null } }], }, }, });