@sanity/sdk-react 3.11.0 → 3.12.0-next.20261030092717

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ import { t as useSanityInstance } from "./useSanityInstance-C0a_Ou_p.js";
2
+ import { c } from "react-compiler-runtime";
3
+ import { getDashboardMessageBus } from "@sanity/sdk/_internal";
4
+ import * as React$1 from "react";
5
+ import { useContext } from "react";
6
+ import "@sanity/sdk/dashboard";
7
+ const ROOT_SLOT_KEY = Symbol.for("sanity.os.root");
8
+ /**
9
+ * Returns the React context that carries the {@link DashboardRoot} the app renders in.
10
+ *
11
+ * @remarks
12
+ * The context lives in a per-React-copy slot on `globalThis` so the CLI wrapper and the SDK
13
+ * share it even across module copies. Each React copy gets its own context, since a context
14
+ * created by one copy is inert in another.
15
+ *
16
+ * The slot is keyed on `React.createContext` rather than the `React` namespace object:
17
+ * `import * as React` and `import React from 'react'` can yield different wrapper objects for
18
+ * the same React copy under bundler interop, whereas the `createContext` function is the same
19
+ * reference under both.
20
+ *
21
+ * The provider side lives in the CLI, which must not import the SDK:
22
+ * `packages/@sanity/workbench-cli/src/actions/build/render-remote.ts` in `sanity-io/cli`
23
+ * reconstructs this accessor (same symbol, same key) and wraps `App` in the context's
24
+ * `Provider`.
25
+ * @internal
26
+ */
27
+ function getDashboardRootContext() {
28
+ let globals = globalThis, slot = globals[ROOT_SLOT_KEY] ??= /* @__PURE__ */ new WeakMap(), key = React$1.createContext, context = slot.get(key);
29
+ return context || (context = React$1.createContext(void 0), slot.set(key, context)), context;
30
+ }
31
+ /**
32
+ * Returns the instance's message bus connection, or `undefined` outside a Dashboard. It connects
33
+ * with the module id and parent key the CLI wrapper provides, so a host reads its own `key` here
34
+ * to pass as the `parentKey` of the modules it renders.
35
+ *
36
+ * @remarks
37
+ * Safe to call on every render: the connection is cached on the instance, so each call returns
38
+ * the same one, and only the first call's identity is used.
39
+ * @internal
40
+ */
41
+ function useMessageBus() {
42
+ let $ = c(5), instance = useSanityInstance(), t0;
43
+ $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = getDashboardRootContext(), $[0] = t0) : t0 = $[0];
44
+ let root = useContext(t0), t1 = root?.moduleId, t2 = root?.parentKey, t3;
45
+ return $[1] !== instance || $[2] !== t1 || $[3] !== t2 ? (t3 = getDashboardMessageBus(instance, t1, t2), $[1] = instance, $[2] = t1, $[3] = t2, $[4] = t3) : t3 = $[4], t3;
46
+ }
47
+ export { getDashboardRootContext as n, useMessageBus as t };
48
+
49
+ //# sourceMappingURL=useMessageBus-BmUySYSG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMessageBus-BmUySYSG.js","names":["React","ROOT_SLOT_KEY","Symbol","for","DashboardRoot","signal","AbortSignal","moduleId","parentKey","RootSlot","WeakMap","createContext","Context","getDashboardRootContext","globals","globalThis","slot","key","context","get","undefined","set","React","getDashboardMessageBus","MessageBusConnection","useContext","getDashboardRootContext","useSanityInstance","useMessageBus","$","_c","instance","t0","Symbol","for","root","t1","moduleId","t2","parentKey","t3","_c"],"sources":["../src/dashboard/module.ts","../src/hooks/dashboard/useMessageBus.ts"],"sourcesContent":["import * as React from 'react'\n\nconst ROOT_SLOT_KEY = Symbol.for('sanity.os.root')\n\n/** The root a CLI-generated wrapper rendered the app into. */\ninterface DashboardRoot {\n /** Aborts when the host disposes the root, never when it hides it. */\n signal: AbortSignal\n /** The federation module id the host rendered, from `renderOptions.moduleId`. */\n moduleId?: string\n /** The key of the host's bus connection, when the host passed `renderOptions.parentKey`. */\n parentKey?: string\n}\n\ntype RootSlot = WeakMap<typeof React.createContext, React.Context<DashboardRoot | undefined>>\n\n/**\n * Returns the React context that carries the {@link DashboardRoot} the app renders in.\n *\n * @remarks\n * The context lives in a per-React-copy slot on `globalThis` so the CLI wrapper and the SDK\n * share it even across module copies. Each React copy gets its own context, since a context\n * created by one copy is inert in another.\n *\n * The slot is keyed on `React.createContext` rather than the `React` namespace object:\n * `import * as React` and `import React from 'react'` can yield different wrapper objects for\n * the same React copy under bundler interop, whereas the `createContext` function is the same\n * reference under both.\n *\n * The provider side lives in the CLI, which must not import the SDK:\n * `packages/@sanity/workbench-cli/src/actions/build/render-remote.ts` in `sanity-io/cli`\n * reconstructs this accessor (same symbol, same key) and wraps `App` in the context's\n * `Provider`.\n * @internal\n */\nexport function getDashboardRootContext(): React.Context<DashboardRoot | undefined> {\n const globals = globalThis as {[ROOT_SLOT_KEY]?: RootSlot}\n const slot = (globals[ROOT_SLOT_KEY] ??= new WeakMap())\n const key = React.createContext\n let context = slot.get(key)\n if (!context) {\n context = React.createContext<DashboardRoot | undefined>(undefined)\n slot.set(key, context)\n }\n return context\n}\n","import {getDashboardMessageBus} from '@sanity/sdk/_internal'\nimport {type MessageBusConnection} from '@sanity/sdk/dashboard'\nimport {useContext} from 'react'\n\nimport {getDashboardRootContext} from '../../dashboard/module'\nimport {useSanityInstance} from '../context/useSanityInstance'\n\n/**\n * Returns the instance's message bus connection, or `undefined` outside a Dashboard. It connects\n * with the module id and parent key the CLI wrapper provides, so a host reads its own `key` here\n * to pass as the `parentKey` of the modules it renders.\n *\n * @remarks\n * Safe to call on every render: the connection is cached on the instance, so each call returns\n * the same one, and only the first call's identity is used.\n * @internal\n */\nexport function useMessageBus(): MessageBusConnection | undefined {\n const instance = useSanityInstance()\n const root = useContext(getDashboardRootContext())\n return getDashboardMessageBus(instance, root?.moduleId, root?.parentKey)\n}\n"],"mappings":";;;;;;AAEA,MAAMC,gBAAgBC,OAAOC,IAAI,gBAAgB;;;;;;;;;;;;;;;;;;;;AAiCjD,SAAgBU,0BAAoE;CAClF,IAAMC,UAAUC,YACVC,OAAQF,QAAQb,mCAAmB,IAAIS,QAAQ,GAC/CO,MAAMjB,QAAMW,eACdO,UAAUF,KAAKG,IAAIF,GAAG;CAK1B,OAJKC,YACHA,UAAUlB,QAAMW,cAAyCS,KAAAA,CAAS,GAClEJ,KAAKK,IAAIJ,KAAKC,OAAO,IAEhBA;AACT;;;;;;;;;;;AC5BA,SAAOU,gBAAA;CAAA,IAAAC,IAAAC,EAAA,CAAA,GACLC,WAAiBJ,kBAAkB,GAACK;CAAA,AAAAH,EAAA,OAAAI,OAAAC,IAAA,2BAAA,KACZF,KAAAN,wBAAwB,GAACG,EAAA,KAAAG,MAAAA,KAAAH,EAAA;CAAjD,IAAAM,OAAaV,WAAWO,EAAyB,GACTI,KAAAD,MAAIE,UAAYC,KAAAH,MAAII,WAAWC;CAAC,OAADX,EAAA,OAAAE,YAAAF,EAAA,OAAAO,MAAAP,EAAA,OAAAS,MAAhEE,KAAAjB,uBAAuBQ,UAAUK,IAAgBE,EAAe,GAACT,EAAA,KAAAE,UAAAF,EAAA,KAAAO,IAAAP,EAAA,KAAAS,IAAAT,EAAA,KAAAW,MAAAA,KAAAX,EAAA,IAAjEW;AAAiE"}
@@ -1,10 +1,10 @@
1
1
  import { t as useSanityInstance } from "./useSanityInstance-C0a_Ou_p.js";
2
+ import { t as useMessageBus } from "./useMessageBus-BmUySYSG.js";
2
3
  import { c } from "react-compiler-runtime";
3
4
  import "@sanity/client";
4
5
  import { AuthStateType, getAuthState, getDashboardOrganizationId, setAuthToken } from "@sanity/sdk";
5
- import { getApplicationOrigin, getDashboardMessageBus, getTopicState, isMessageBusEnvironment, requireDashboardMessageBus, resolveTopic } from "@sanity/sdk/_internal";
6
- import * as React$1 from "react";
7
- import { useContext, useEffect, useRef, useState, useSyncExternalStore } from "react";
6
+ import { getApplicationOrigin, getTopicState, isMessageBusEnvironment, requireDashboardMessageBus, resolveTopic } from "@sanity/sdk/_internal";
7
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
8
8
  import { SDK_CHANNEL_NAME, SDK_NODE_NAME } from "@sanity/message-protocol";
9
9
  import { getNodeState } from "@sanity/sdk/comlink";
10
10
  import { defer, filter, firstValueFrom, of } from "rxjs";
@@ -95,47 +95,6 @@ function useWindowConnection(t0) {
95
95
  function _temp$1(unsubscribe) {
96
96
  return unsubscribe();
97
97
  }
98
- const MODULE_SLOT_KEY = Symbol.for("sanity.os.module"), ROOT_SLOT_KEY = Symbol.for("sanity.os.root");
99
- function getSlotContext(slotKey) {
100
- let globals = globalThis, slot = globals[slotKey] ??= /* @__PURE__ */ new WeakMap(), key = React$1.createContext, context = slot.get(key);
101
- return context || (context = React$1.createContext(void 0), slot.set(key, context)), context;
102
- }
103
- /**
104
- * Returns the React context that carries the current federation module id.
105
- *
106
- * @remarks
107
- * This is the slot the CLI-generated wrapper populates with
108
- * `renderOptions.moduleId`. The context lives in a per-React-copy slot on
109
- * `globalThis` so the CLI wrapper and the SDK share the same context even
110
- * across module copies. Each React copy gets its own context, since a context
111
- * created by one copy is inert in another.
112
- *
113
- * The slot is keyed on `React.createContext` rather than the `React` namespace
114
- * object: `import * as React` and `import React from 'react'` can yield
115
- * different wrapper objects for the same React copy under bundler interop,
116
- * whereas the `createContext` function is the same reference under both.
117
- *
118
- * The provider side lives in the CLI, which must not import the SDK:
119
- * `packages/@sanity/workbench-cli/src/actions/build/render-remote.ts` in
120
- * `sanity-io/cli` reconstructs this accessor (same symbol, same key) and
121
- * wraps `App` in the context's `Provider`. Nothing in this repo exports it.
122
- * @internal
123
- */
124
- function getDashboardModuleContext() {
125
- return getSlotContext(MODULE_SLOT_KEY);
126
- }
127
- /**
128
- * Returns the React context that carries the {@link DashboardRoot} the app renders in.
129
- *
130
- * @remarks
131
- * Shared with the CLI-generated wrapper the same way as
132
- * {@link getDashboardModuleContext}, under `Symbol.for('sanity.os.root')`. It is a
133
- * separate context because older SDKs read the module context as a plain string.
134
- * @internal
135
- */
136
- function getDashboardRootContext() {
137
- return getSlotContext(ROOT_SLOT_KEY);
138
- }
139
98
  /**
140
99
  * Keeps the SDK auth token in sync with the dashboard "OS".
141
100
  *
@@ -207,17 +166,13 @@ function _temp() {
207
166
  return of(null);
208
167
  }
209
168
  const DashboardTokenRefreshProvider = (t0) => {
210
- let $ = c(7), { children } = t0, instance = useSanityInstance(), t1;
211
- $[0] === Symbol.for("react.memo_cache_sentinel") ? (t1 = getDashboardModuleContext(), $[0] = t1) : t1 = $[0];
212
- let moduleId = useContext(t1), t2;
213
- $[1] !== instance || $[2] !== moduleId ? (t2 = () => getDashboardMessageBus(instance, moduleId), $[1] = instance, $[2] = moduleId, $[3] = t2) : t2 = $[3];
214
- let [messageBus] = useState(t2);
169
+ let $ = c(3), { children } = t0, messageBus = useMessageBus();
215
170
  if (messageBus) {
216
- let t3;
217
- return $[4] !== children || $[5] !== messageBus ? (t3 = /* @__PURE__ */ jsx(DashboardTokenRefresh, {
171
+ let t1;
172
+ return $[0] !== children || $[1] !== messageBus ? (t1 = /* @__PURE__ */ jsx(DashboardTokenRefresh, {
218
173
  messageBus,
219
174
  children
220
- }), $[4] = children, $[5] = messageBus, $[6] = t3) : t3 = $[6], t3;
175
+ }), $[0] = children, $[1] = messageBus, $[2] = t1) : t1 = $[2], t1;
221
176
  }
222
177
  return children;
223
178
  };
@@ -461,6 +416,6 @@ function useComlinkStudioWorkspaces() {
461
416
  error
462
417
  }, $[5] = error, $[6] = workspacesByProjectIdAndDataset, $[7] = t4) : t4 = $[7], t4;
463
418
  }
464
- export { DashboardTokenRefreshProvider as a, useAuthState as c, useOrganizationId as i, createStateSourceHook as l, useTopic as n, getDashboardRootContext as o, useEmit as r, useWindowConnection as s, useStudioWorkspacesByProjectIdDataset as t };
419
+ export { DashboardTokenRefreshProvider as a, createStateSourceHook as c, useOrganizationId as i, useTopic as n, useWindowConnection as o, useEmit as r, useAuthState as s, useStudioWorkspacesByProjectIdDataset as t };
465
420
 
466
- //# sourceMappingURL=useStudioWorkspacesByProjectIdDataset-Cx-Q44d0.js.map
421
+ //# sourceMappingURL=useStudioWorkspacesByProjectIdDataset-BX72KXvj.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStudioWorkspacesByProjectIdDataset-BX72KXvj.js","names":["SanityConfig","SanityInstance","StateSource","useSyncExternalStore","useSanityInstance","StateSourceFactory","instance","params","TParams","TState","CreateStateSourceHookOptions","getState","shouldSuspend","suspender","Promise","getConfig","createStateSourceHook","options","suspense","undefined","useHook","t0","$","_c","t1","state","subscribe","getCurrent","_c","AuthState","getAuthState","createStateSourceHook","useAuthState","MessageData","NodeInput","SanityInstance","StateSource","FrameMessage","getNodeState","NodeState","WindowMessage","useCallback","useEffect","useRef","filter","firstValueFrom","useSanityInstance","createStateSourceHook","WindowMessageHandler","event","TFrameMessage","UseWindowConnectionOptions","name","connectTo","onMessage","Record","TMessage","WindowConnection","sendMessage","type","TType","data","Extract","fetch","options","signal","AbortSignal","suppressWarnings","responseTimeout","Promise","TResponse","useNodeState","getState","instance","nodeInput","shouldSuspend","getCurrent","undefined","suspender","observable","pipe","Boolean","useWindowConnection","t0","$","_c","t1","node","t2","Symbol","for","messageUnsubscribers","t3","Object","entries","forEach","t4","handler","messageUnsubscribe","on","current","push","_temp","t5","type_0","post","t6","type_1","data_0","fetchOptions","t7","unsubscribe","_c","_temp","ClientError","AuthStateType","setAuthToken","MessageBus","React","PropsWithChildren","useEffect","useRef","defer","of","catchError","useAuthState","useSanityInstance","useMessageBus","DashboardTokenRefresh","t0","$","_c","children","messageBus","instance","authState","processed401ErrorRef","t1","t2","subscription","subscribe","pipe","_temp","token","unsubscribe","t3","error","type","has401Error","ERROR","statusCode","current","emit","undefined","catch","_temp2","t4","console","warn","DashboardTokenRefreshProvider","FC","_c","getDashboardOrganizationId","OrganizationBase","getTopicState","isMessageBusEnvironment","useMemo","useSyncExternalStore","useSanityInstance","CurrentOrganization","Pick","useOrganizationId","$","_c","instance","t0","bb0","t1","source","t2","getCurrent","id","undefined","t3","subscribe","_c","requireDashboardMessageBus","EventTopic","MessageBusEmitOptions","MessageBusEmitResult","PayloadOf","ReplyOf","useCallback","useSanityInstance","TopicEmitter","args","K","payload","options","useEmit","topic","$","_c","instance","t0","t1","messageBus","t2","t3","emit","_c","getTopicState","resolveTopic","StateTopic","TopicData","createStateSourceHook","useTopic","getState","shouldSuspend","instance","topic","getCurrent","undefined","suspender","K","SDK_CHANNEL_NAME","SDK_NODE_NAME","getApplicationOrigin","isMessageBusEnvironment","TopicData","useEffect","useMemo","useState","useWindowConnection","useTopic","DashboardResource","id","name","title","basePath","projectId","dataset","type","userApplicationId","userApplicationSlug","url","WorkspacesByProjectIdDataset","key","StudioWorkspacesResult","workspacesByProjectIdAndDataset","error","DashboardApplications","NonNullable","useStudioWorkspacesByProjectIdDataset","useBusStudioWorkspaces","useComlinkStudioWorkspaces","toResources","application","activeDeployment","workspaces","map","workspace","slug","toWorkspaceMap","applications","workspaceMap","resource","flatMap","const","push","$","_c","t0","t1","t2","Symbol","for","setWorkspacesByProjectIdAndDataset","setError","connectTo","fetch","t3","fetchWorkspaces","signal","data","undefined","noProjectIdAndDataset","context","availableResources","forEach","length","t4","err","Error","controller","AbortController","abort","_c"],"sources":["../src/hooks/helpers/createStateSourceHook.tsx","../src/hooks/auth/useAuthState.tsx","../src/hooks/comlink/useWindowConnection.ts","../src/context/DashboardTokenRefresh.tsx","../src/hooks/dashboard/useOrganizationId.tsx","../src/hooks/dashboard/useEmit.ts","../src/hooks/dashboard/useTopic.ts","../src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts"],"sourcesContent":["import {type SanityConfig, type SanityInstance, type StateSource} from '@sanity/sdk'\nimport {useSyncExternalStore} from 'react'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\n\ntype StateSourceFactory<TParams extends unknown[], TState> = (\n instance: SanityInstance,\n ...params: TParams\n) => StateSource<TState>\n\ninterface CreateStateSourceHookOptions<TParams extends unknown[], TState> {\n getState: StateSourceFactory<TParams, TState>\n shouldSuspend?: (instance: SanityInstance, ...params: TParams) => boolean\n suspender?: (instance: SanityInstance, ...params: TParams) => Promise<unknown>\n getConfig?: (...params: TParams) => SanityConfig | undefined\n}\n\nexport function createStateSourceHook<TParams extends unknown[], TState>(\n options: StateSourceFactory<TParams, TState> | CreateStateSourceHookOptions<TParams, TState>,\n): (...params: TParams) => TState {\n const getState = typeof options === 'function' ? options : options.getState\n const suspense = 'shouldSuspend' in options && 'suspender' in options ? options : undefined\n\n function useHook(...params: TParams) {\n const instance = useSanityInstance()\n\n if (suspense?.suspender && suspense?.shouldSuspend?.(instance, ...params)) {\n throw suspense.suspender(instance, ...params)\n }\n\n const state = getState(instance, ...params)\n return useSyncExternalStore(state.subscribe, state.getCurrent)\n }\n\n return useHook\n}\n","import {type AuthState, getAuthState} from '@sanity/sdk'\n\nimport {createStateSourceHook} from '../helpers/createStateSourceHook'\n\n/**\n * @internal\n * A React hook that subscribes to authentication state changes.\n *\n * This hook provides access to the current authentication state type from the Sanity auth store.\n * It automatically re-renders when the authentication state changes.\n *\n * @remarks\n * The hook uses `useSyncExternalStore` to safely subscribe to auth state changes\n * and ensure consistency between server and client rendering.\n *\n * @returns The current authentication state type\n *\n * @example\n * ```tsx\n * function AuthStatus() {\n * const authState = useAuthState()\n * return <div>Current auth state: {authState}</div>\n * }\n * ```\n */\nexport const useAuthState: () => AuthState = createStateSourceHook(getAuthState)\n","import {type MessageData, type NodeInput} from '@sanity/comlink'\nimport {type SanityInstance, type StateSource} from '@sanity/sdk'\nimport {\n type FrameMessage,\n getNodeState,\n type NodeState,\n type WindowMessage,\n} from '@sanity/sdk/comlink'\nimport {useCallback, useEffect, useRef} from 'react'\nimport {filter, firstValueFrom} from 'rxjs'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\nimport {createStateSourceHook} from '../helpers/createStateSourceHook'\n\n/**\n * @internal\n */\nexport type WindowMessageHandler<TFrameMessage extends FrameMessage> = (\n event: TFrameMessage['data'],\n) => TFrameMessage['response']\n\n/**\n * @internal\n */\nexport interface UseWindowConnectionOptions<TMessage extends FrameMessage> {\n name: string\n connectTo: string\n onMessage?: Record<TMessage['type'], WindowMessageHandler<TMessage>>\n}\n\n/**\n * @internal\n */\nexport interface WindowConnection<TMessage extends WindowMessage> {\n sendMessage: <TType extends TMessage['type']>(\n type: TType,\n data?: Extract<TMessage, {type: TType}>['data'],\n ) => void\n fetch: <TResponse>(\n type: string,\n data?: MessageData,\n options?: {\n signal?: AbortSignal\n suppressWarnings?: boolean\n responseTimeout?: number\n },\n ) => Promise<TResponse>\n}\n\nconst useNodeState = createStateSourceHook({\n getState: getNodeState as (\n instance: SanityInstance,\n nodeInput: NodeInput,\n ) => StateSource<NodeState>,\n shouldSuspend: (instance: SanityInstance, nodeInput: NodeInput) =>\n getNodeState(instance, nodeInput).getCurrent() === undefined,\n suspender: (instance: SanityInstance, nodeInput: NodeInput) => {\n return firstValueFrom(getNodeState(instance, nodeInput).observable.pipe(filter(Boolean)))\n },\n})\n\n/**\n * @internal\n * Hook to wrap a Comlink node in a React hook.\n * Our store functionality takes care of the lifecycle of the node,\n * as well as sharing a single node between invocations if they share the same name.\n *\n * Generally not to be used directly, but to be used as a dependency of\n * Comlink-powered hooks like `useStudioWorkspacesByProjectIdDataset`.\n */\nexport function useWindowConnection<\n TWindowMessage extends WindowMessage,\n TFrameMessage extends FrameMessage,\n>({\n name,\n connectTo,\n onMessage,\n}: UseWindowConnectionOptions<TFrameMessage>): WindowConnection<TWindowMessage> {\n const {node} = useNodeState({name, connectTo})\n const messageUnsubscribers = useRef<(() => void)[]>([])\n const instance = useSanityInstance()\n\n useEffect(() => {\n if (onMessage) {\n Object.entries(onMessage).forEach(([type, handler]) => {\n const messageUnsubscribe = node.on(type, handler as WindowMessageHandler<TFrameMessage>)\n if (messageUnsubscribe) {\n messageUnsubscribers.current.push(messageUnsubscribe)\n }\n })\n }\n\n return () => {\n messageUnsubscribers.current.forEach((unsubscribe) => unsubscribe())\n messageUnsubscribers.current = []\n }\n }, [instance, name, onMessage, node])\n\n const sendMessage = useCallback(\n (type: TWindowMessage['type'], data?: Extract<TWindowMessage, {type: typeof type}>['data']) => {\n node.post(type, data)\n },\n [node],\n )\n\n const fetch = useCallback(\n <TResponse>(\n type: string,\n data?: MessageData,\n fetchOptions?: {\n responseTimeout?: number\n signal?: AbortSignal\n suppressWarnings?: boolean\n },\n ): Promise<TResponse> => {\n return node.fetch(type, data, fetchOptions ?? {}) as Promise<TResponse>\n },\n [node],\n )\n return {\n sendMessage,\n fetch,\n }\n}\n","import {type ClientError} from '@sanity/client'\nimport {AuthStateType, setAuthToken} from '@sanity/sdk'\nimport {type MessageBus} from '@sanity/sdk/dashboard'\nimport React, {type PropsWithChildren, useEffect, useRef} from 'react'\nimport {defer, of} from 'rxjs'\nimport {catchError} from 'rxjs/operators'\n\nimport {useAuthState} from '../hooks/auth/useAuthState'\nimport {useSanityInstance} from '../hooks/context/useSanityInstance'\nimport {useMessageBus} from '../hooks/dashboard/useMessageBus'\n\n/**\n * Keeps the SDK auth token in sync with the dashboard \"OS\".\n *\n * When running inside the dashboard the OS owns the session, so we subscribe\n * to its `auth.token` stream and mirror each value into\n * the auth store — a token logs us in, `null` logs us out, and later OS\n * sign-in/out propagates automatically. When a request is rejected with a 401\n * (the token expired), we ask the OS to reissue rather than tearing the session\n * down; the new token arrives back through the same subscription.\n */\nfunction DashboardTokenRefresh({\n children,\n messageBus,\n}: PropsWithChildren<{messageBus: MessageBus}>) {\n const instance = useSanityInstance()\n const authState = useAuthState()\n const processed401ErrorRef = useRef<unknown | null>(null)\n\n useEffect(() => {\n const subscription = defer(() => messageBus.subscribe('auth.token'))\n .pipe(catchError(() => of(null)))\n .subscribe((token) => setAuthToken(instance, token))\n return () => subscription.unsubscribe()\n }, [instance, messageBus])\n\n useEffect(() => {\n const has401Error =\n authState.type === AuthStateType.ERROR && (authState.error as ClientError)?.statusCode === 401\n\n if (has401Error && processed401ErrorRef.current !== authState.error) {\n processed401ErrorRef.current = authState.error\n // Event topics have no replay, so a missing responder or timeout is otherwise dropped silently.\n messageBus.emit('auth.token.refresh', undefined).catch((error) => {\n // eslint-disable-next-line no-console\n console.warn('[sanity/sdk] Dashboard token refresh failed:', error)\n })\n } else if (!has401Error) {\n processed401ErrorRef.current = null\n }\n }, [authState, messageBus])\n\n return children\n}\n\n/**\n * Authenticates the SDK with the Sanity Dashboard's session when the app runs\n * inside the dashboard.\n *\n * The dashboard owns the session there: this provider subscribes to the token\n * the dashboard issues, writes each new value into the SDK's auth store (where\n * SDK hooks read it from), and asks the dashboard for a fresh token when a\n * request fails with a 401. Outside the dashboard it renders children\n * unchanged and the app's normal auth flow applies.\n *\n * @remarks\n * `AuthBoundary` mounts this automatically, so most apps never need it\n * directly. Mount it yourself only when your app runs inside the dashboard\n * without `AuthBoundary` — that is, the app renders its own loading and error\n * UI instead of the SDK's login flow — but still uses SDK hooks such as\n * `useQuery`, which need the dashboard's token in the auth store to\n * authenticate their requests.\n *\n * Mount it once, inside the provider that creates the Sanity instance whose\n * store should receive the token.\n *\n * @example\n * ```tsx\n * import {ResourceProvider} from '@sanity/sdk-react'\n * import {TokenRefreshProvider} from '@sanity/sdk-react/dashboard'\n *\n * function EmbeddedApp() {\n * return (\n * <ResourceProvider fallback={<Loading />}>\n * <TokenRefreshProvider>\n * <App />\n * </TokenRefreshProvider>\n * </ResourceProvider>\n * )\n * }\n * ```\n *\n * @public\n * @category Dashboard\n */\nexport const DashboardTokenRefreshProvider: React.FC<PropsWithChildren> = ({children}) => {\n // The connection is first-caller-wins per instance, and hooks below read it during their\n // render, before any effect here could run. Connecting in the first render pins the module\n // identity before they do; nothing has subscribed to the store yet, so the write is safe.\n const messageBus = useMessageBus()\n if (messageBus) {\n return <DashboardTokenRefresh messageBus={messageBus}>{children}</DashboardTokenRefresh>\n }\n\n return children\n}\n","import {getDashboardOrganizationId, type OrganizationBase} from '@sanity/sdk'\nimport {getTopicState, isMessageBusEnvironment} from '@sanity/sdk/_internal'\nimport {useMemo, useSyncExternalStore} from 'react'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\n\ntype CurrentOrganization = Pick<OrganizationBase, 'id' | 'name' | 'slug'> | null | undefined\n\n/**\n * @public\n *\n * A React hook that retrieves the dashboard organization ID that is currently selected in the Sanity Dashboard.\n *\n * Works in both Dashboard runtimes: it reads the `organizations.current` message bus topic when a\n * host has installed the bus, and falls back to the Comlink connection otherwise.\n *\n * @example\n * ```tsx\n * function DashboardComponent() {\n * const orgId = useOrganizationId()\n *\n * if (!orgId) return null\n *\n * return <div>Organization ID: {String(orgId)}</div>\n * }\n * ```\n *\n * @category Dashboard\n * @returns The dashboard organization ID (string | undefined)\n */\nexport function useOrganizationId(): string | undefined {\n const instance = useSanityInstance()\n const {subscribe, getCurrent} = useMemo(() => {\n if (!isMessageBusEnvironment()) return getDashboardOrganizationId(instance)\n const source = getTopicState(instance, 'organizations.current')\n return {\n subscribe: source.subscribe,\n getCurrent: () => (source.getCurrent() as CurrentOrganization)?.id ?? undefined,\n }\n }, [instance])\n\n return useSyncExternalStore(subscribe, getCurrent)\n}\n","import {requireDashboardMessageBus} from '@sanity/sdk/_internal'\nimport {\n type EventTopic,\n type MessageBusEmitOptions,\n type MessageBusEmitResult,\n type PayloadOf,\n type ReplyOf,\n} from '@sanity/sdk/dashboard'\nimport {useCallback} from 'react'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\n\n/**\n * Emits a dashboard event topic, typed to the topic's payload and reply.\n * @public\n * @category Types\n * @category Dashboard\n */\nexport type TopicEmitter<K extends EventTopic> = (\n ...args: PayloadOf<K> extends void\n ? [payload?: void, options?: MessageBusEmitOptions]\n : [payload: PayloadOf<K>, options?: MessageBusEmitOptions]\n) => MessageBusEmitResult<ReplyOf<K>>\n\n/**\n * Returns a stable function that emits a dashboard event topic.\n *\n * The event is sent immediately. Ignore the lazy result for fire-and-forget delivery, await it\n * inside `useTransition` to track a reply, or read it with React `use` to suspend. A reply\n * that fails (`NO_RESPONDER`, `TIMEOUT`, `ABORTED`) rejects the result; when read with `use`\n * that reaches the nearest error boundary, so pair the `Suspense` with one.\n *\n * @example Fire and forget\n * ```tsx\n * function ExpandPanel() {\n * const setPanelMode = useEmit('panels.mode.set')\n * return (\n * <button onClick={() => setPanelMode({name: 'favorites', mode: 'full'})}>\n * Expand\n * </button>\n * )\n * }\n * ```\n *\n * @example Await a reply with Suspense\n * ```tsx\n * function Session({request}: {request: MessageBusEmitResult<string>}) {\n * use(request)\n * return <p>Ready</p>\n * }\n *\n * function SessionAccordion() {\n * const refreshToken = useEmit('auth.token.refresh')\n * const [request, setRequest] = useState<MessageBusEmitResult<string> | null>(null)\n *\n * return (\n * <details\n * onToggle={(event) => setRequest(event.currentTarget.open ? refreshToken() : null)}\n * >\n * <summary>Session</summary>\n * <ErrorBoundary fallback={<p>Could not refresh</p>}>\n * <Suspense fallback={<p>Refreshing...</p>}>\n * {request && <Session request={request} />}\n * </Suspense>\n * </ErrorBoundary>\n * </details>\n * )\n * }\n * ```\n *\n * @public\n * @category Dashboard\n */\nexport function useEmit<K extends EventTopic>(topic: K): TopicEmitter<K> {\n const instance = useSanityInstance()\n const messageBus = requireDashboardMessageBus(instance, `emit topic \"${topic}\"`)\n return useCallback<TopicEmitter<K>>(\n (...args) => messageBus.emit(topic, ...args),\n [messageBus, topic],\n )\n}\n","import {getTopicState, resolveTopic} from '@sanity/sdk/_internal'\nimport {type StateTopic, type TopicData} from '@sanity/sdk/dashboard'\n\nimport {createStateSourceHook} from '../helpers/createStateSourceHook'\n\n/**\n * Returns the current value of a dashboard state topic and follows later updates.\n *\n * The hook suspends until the topic publishes its first value, using the message bus query\n * deadline. A topic declared with `TopicResult` resolves to its successful value; a\n * failed result throws a `TopicError` to the nearest error boundary.\n *\n * @example\n * ```tsx\n * function ForegroundApplication() {\n * const foregroundId = useTopic('applications.foreground')\n * return <span>{foregroundId ?? 'No application in the foreground'}</span>\n * }\n * ```\n *\n * @public\n * @category Dashboard\n */\nexport const useTopic = createStateSourceHook({\n getState: getTopicState,\n // `getCurrent` throws a recorded read failure, which surfaces it from render like a thrown value.\n shouldSuspend: (instance, topic: StateTopic) =>\n getTopicState(instance, topic).getCurrent() === undefined,\n suspender: resolveTopic,\n}) as <K extends StateTopic>(topic: K) => TopicData<K>\n","/* eslint-disable react-compiler/react-compiler -- the transport branch in `useStudioWorkspacesByProjectIdDataset` is a deliberate rules-of-hooks exception; the compiler refuses files that disable it */\nimport {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'\nimport {getApplicationOrigin, isMessageBusEnvironment} from '@sanity/sdk/_internal'\nimport {type TopicData} from '@sanity/sdk/dashboard'\nimport {useEffect, useMemo, useState} from 'react'\n\nimport {useWindowConnection} from '../comlink/useWindowConnection'\nimport {useTopic} from './useTopic'\n\nexport interface DashboardResource {\n id: string\n name: string\n title: string\n basePath: string\n projectId: string\n dataset: string\n type: string\n userApplicationId: string\n userApplicationSlug?: string | null\n url: string\n}\n\ninterface WorkspacesByProjectIdDataset {\n [key: `${string}:${string}`]: DashboardResource[] // key format: `${projectId}:${dataset}`\n}\n\ninterface StudioWorkspacesResult {\n workspacesByProjectIdAndDataset: WorkspacesByProjectIdDataset\n error: string | null\n}\n\ntype DashboardApplications = NonNullable<TopicData<'applications.list'>>\n\n/**\n * Hook that fetches studio workspaces and organizes them by projectId:dataset\n *\n * Works in both Dashboard runtimes: it derives workspaces from the `applications.list` message\n * bus topic when a host has installed the bus, and falls back to the Comlink connection otherwise.\n * @internal\n *\n * @example\n * ```tsx\n * import {useStudioWorkspacesByProjectIdDataset} from '@sanity/sdk-react'\n * import {Card, Code, Button} from '@sanity/ui'\n * import {Suspense} from 'react'\n *\n * function WorkspacesCard() {\n * const {workspacesByProjectIdAndDataset, error} = useStudioWorkspacesByProjectIdDataset()\n * if (error) {\n * return <div>Error: {error}</div>\n * }\n * return (\n * <Card padding={4} radius={2} shadow={1}>\n * <Code language=\"json\">\n * {JSON.stringify(workspacesByProjectIdAndDataset, null, 2)}\n * </Code>\n * </Card>\n * )\n * }\n *\n * // Wrap the component with Suspense since the hook may suspend\n * function DashboardWorkspaces() {\n * return (\n * <Suspense fallback={<Button text=\"Loading...\" disabled />}>\n * <WorkspacesCard />\n * </Suspense>\n * )\n * }\n * ```\n */\nexport function useStudioWorkspacesByProjectIdDataset(): StudioWorkspacesResult {\n // The branch is stable: the transport is fixed for the page lifetime, so one set of hooks\n // always runs and the other never does.\n // eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime\n if (isMessageBusEnvironment()) return useBusStudioWorkspaces()\n // eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime\n return useComlinkStudioWorkspaces()\n}\n\n// The legacy Comlink protocol models studios at the workspace level, so each workspace of a\n// studio's active deployment becomes one resource, addressed by the studio's origin.\nfunction toResources(application: DashboardApplications[number]): DashboardResource[] {\n // Installations join the topic union without a `type`; only studios yield workspace resources.\n if (!('type' in application) || application.type !== 'studio') return []\n const url = getApplicationOrigin(application) ?? ''\n return (application.activeDeployment?.workspaces ?? []).map((workspace) => ({\n id: workspace.id,\n name: workspace.name,\n title: workspace.title ?? application.title,\n basePath: workspace.basePath ?? '',\n projectId: workspace.projectId,\n dataset: workspace.dataset,\n type: 'studio',\n userApplicationId: application.id,\n userApplicationSlug: application.slug,\n url,\n }))\n}\n\nfunction toWorkspaceMap(applications: DashboardApplications): WorkspacesByProjectIdDataset {\n const workspaceMap: WorkspacesByProjectIdDataset = {}\n for (const resource of applications.flatMap(toResources)) {\n const key = `${resource.projectId}:${resource.dataset}` as const\n workspaceMap[key] ??= []\n workspaceMap[key].push(resource)\n }\n return workspaceMap\n}\n\n// Suspends until the host publishes its application list and throws a `TopicError` on failure,\n// like every bus hook, so `error` is always `null` on this path.\nfunction useBusStudioWorkspaces(): StudioWorkspacesResult {\n const applications = useTopic('applications.list')\n const workspacesByProjectIdAndDataset = useMemo(\n () => toWorkspaceMap(applications ?? []),\n [applications],\n )\n return {workspacesByProjectIdAndDataset, error: null}\n}\n\nfunction useComlinkStudioWorkspaces(): StudioWorkspacesResult {\n const [workspacesByProjectIdAndDataset, setWorkspacesByProjectIdAndDataset] =\n useState<WorkspacesByProjectIdDataset>({})\n const [error, setError] = useState<string | null>(null)\n\n const {fetch} = useWindowConnection({\n name: SDK_NODE_NAME,\n connectTo: SDK_CHANNEL_NAME,\n })\n\n // Once computed, this should probably be in a store and poll for changes\n // However, our stores are currently being refactored\n useEffect(() => {\n if (!fetch) return\n\n async function fetchWorkspaces(signal: AbortSignal) {\n try {\n const data = await fetch<{\n context: {availableResources: Array<DashboardResource>}\n }>('dashboard/v1/context', undefined, {signal})\n\n const workspaceMap: WorkspacesByProjectIdDataset = {}\n const noProjectIdAndDataset: DashboardResource[] = []\n\n data.context.availableResources.forEach((resource) => {\n if (resource.type !== 'studio') return\n if (!resource.projectId || !resource.dataset) {\n noProjectIdAndDataset.push(resource)\n return\n }\n const key = `${resource.projectId}:${resource.dataset}` as const\n if (!workspaceMap[key]) {\n workspaceMap[key] = []\n }\n workspaceMap[key].push(resource)\n })\n\n if (noProjectIdAndDataset.length > 0) {\n workspaceMap['NO_PROJECT_ID:NO_DATASET'] = noProjectIdAndDataset\n }\n\n setWorkspacesByProjectIdAndDataset(workspaceMap)\n setError(null)\n } catch (err: unknown) {\n if (err instanceof Error) {\n if (err.name === 'AbortError') {\n return\n }\n setError('Failed to fetch workspaces')\n }\n }\n }\n\n const controller = new AbortController()\n fetchWorkspaces(controller.signal)\n\n return () => {\n controller.abort()\n }\n }, [fetch])\n\n return {\n workspacesByProjectIdAndDataset,\n error,\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk-react",
3
- "version": "3.11.0",
3
+ "version": "3.12.0-next.20261030092717",
4
4
  "private": false,
5
5
  "description": "Sanity SDK React toolkit for Content OS",
6
6
  "keywords": [
@@ -46,7 +46,7 @@
46
46
  "@module-federation/runtime": "^2.9.2",
47
47
  "@sanity/client": "^8.13.0",
48
48
  "@sanity/message-protocol": "^0.24.0",
49
- "@sanity/sdk": "3.11.0",
49
+ "@sanity/sdk": "3.12.0-next.20261030092717",
50
50
  "@sanity/types": "^6.21.0",
51
51
  "react-compiler-runtime": "^1.0.0",
52
52
  "react-error-boundary": "^6.1.6",
@@ -69,7 +69,7 @@
69
69
  "@vitejs/plugin-react": "^6.1.2",
70
70
  "@vitest/coverage-v8": "^5.0.5",
71
71
  "babel-plugin-react-compiler": "^1.0.0",
72
- "groq": "^6.20.0",
72
+ "groq": "^6.21.0",
73
73
  "groq-js": "^2.0.1",
74
74
  "jsdom": "^30.1.3",
75
75
  "oxfmt": "^0.75.0",
@@ -1,3 +1,4 @@
1
+ export {useMessageBus} from '../hooks/dashboard/useMessageBus'
1
2
  export {
2
3
  type ApplicationStatus,
3
4
  type ApplicationStatusUpdate,
@@ -1,14 +1,15 @@
1
- import {AuthStateType, setAuthToken} from '@sanity/sdk'
1
+ import {AuthStateType, createSanityInstance, type SanityInstance, setAuthToken} from '@sanity/sdk'
2
2
  import {getDashboardMessageBus} from '@sanity/sdk/_internal'
3
3
  import {act, render} from '@testing-library/react'
4
4
  import {of} from 'rxjs'
5
5
  import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
6
6
 
7
- import {getDashboardModuleContext} from '../dashboard/module'
7
+ import {contextLikeTheCliWrapper} from '../../test/dashboardSlots'
8
8
  import {useAuthState} from '../hooks/auth/useAuthState'
9
9
  import {useSanityInstance} from '../hooks/context/useSanityInstance'
10
10
  import {DashboardTokenRefreshProvider} from './DashboardTokenRefresh'
11
11
  import {ResourceProvider} from './ResourceProvider'
12
+ import {SanityInstanceProvider} from './SanityInstanceProvider'
12
13
 
13
14
  const messageBus = vi.hoisted(() => ({
14
15
  client: undefined as
@@ -38,6 +39,12 @@ vi.mock('@sanity/sdk/_internal', async (importOriginal) => ({
38
39
  }))
39
40
 
40
41
  const mockSetAuthToken = setAuthToken as Mock
42
+ const RootContext = contextLikeTheCliWrapper<{
43
+ signal: AbortSignal
44
+ moduleId?: string
45
+ parentKey?: string
46
+ }>('sanity.os.root')
47
+ const signal = new AbortController().signal
41
48
  const mockUseAuthState = useAuthState as Mock
42
49
 
43
50
  const renderProvider = () =>
@@ -102,7 +109,6 @@ describe('DashboardTokenRefreshProvider', () => {
102
109
  // getDashboardMessageBus is first-caller-wins per instance. A hook reading the bus in
103
110
  // its render runs before any parent effect, so the provider must connect during render
104
111
  // or the connection is pinned to the app id.
105
- const ModuleContext = getDashboardModuleContext()
106
112
  function ReadsBus() {
107
113
  getDashboardMessageBus(useSanityInstance())
108
114
  return null
@@ -110,41 +116,68 @@ describe('DashboardTokenRefreshProvider', () => {
110
116
 
111
117
  act(() => {
112
118
  render(
113
- <ModuleContext.Provider value="favorites/views/list/panel">
119
+ <RootContext.Provider value={{signal, moduleId: 'favorites/views/list/panel'}}>
114
120
  <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
115
121
  <DashboardTokenRefreshProvider>
116
122
  <ReadsBus />
117
123
  </DashboardTokenRefreshProvider>
118
124
  </ResourceProvider>
119
- </ModuleContext.Provider>,
125
+ </RootContext.Provider>,
120
126
  )
121
127
  })
122
128
 
123
129
  expect(messageBus.getDashboardMessageBus.mock.calls[0]).toEqual([
124
130
  expect.anything(),
125
131
  'favorites/views/list/panel',
132
+ undefined,
126
133
  ])
127
134
  })
128
135
 
129
- it('forwards the module id from the dashboard module context', () => {
130
- const ModuleContext = getDashboardModuleContext()
131
-
136
+ it('forwards the module id and parent key the host rendered the root with', () => {
132
137
  act(() => {
133
138
  render(
134
- <ModuleContext.Provider value="favorites/views/list/panel">
139
+ <RootContext.Provider
140
+ value={{signal, moduleId: 'favorites/views/inbox/tile', parentKey: 'home-key'}}
141
+ >
135
142
  <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
136
143
  <DashboardTokenRefreshProvider>
137
144
  <div>Test</div>
138
145
  </DashboardTokenRefreshProvider>
139
146
  </ResourceProvider>
140
- </ModuleContext.Provider>,
147
+ </RootContext.Provider>,
141
148
  )
142
149
  })
143
150
 
144
151
  expect(messageBus.getDashboardMessageBus).toHaveBeenCalledWith(
145
152
  expect.anything(),
146
- 'favorites/views/list/panel',
153
+ 'favorites/views/inbox/tile',
154
+ 'home-key',
155
+ )
156
+ })
157
+
158
+ it('reads the bus of the instance it renders under now, not the first one', () => {
159
+ const first = createSanityInstance({projectId: 'p', dataset: 'd'})
160
+ const second = createSanityInstance({projectId: 'p', dataset: 'd'})
161
+ const under = (instance: SanityInstance) => (
162
+ <SanityInstanceProvider instance={instance} fallback={null}>
163
+ <DashboardTokenRefreshProvider>
164
+ <div>Test</div>
165
+ </DashboardTokenRefreshProvider>
166
+ </SanityInstanceProvider>
167
+ )
168
+
169
+ const {rerender} = render(under(first))
170
+ act(() => {
171
+ rerender(under(second))
172
+ })
173
+
174
+ expect(messageBus.getDashboardMessageBus).toHaveBeenLastCalledWith(
175
+ second,
176
+ undefined,
177
+ undefined,
147
178
  )
179
+ first.dispose()
180
+ second.dispose()
148
181
  })
149
182
 
150
183
  it('treats subscription failures as a missing token', () => {
@@ -1,14 +1,13 @@
1
1
  import {type ClientError} from '@sanity/client'
2
2
  import {AuthStateType, setAuthToken} from '@sanity/sdk'
3
- import {getDashboardMessageBus} from '@sanity/sdk/_internal'
4
3
  import {type MessageBus} from '@sanity/sdk/dashboard'
5
- import React, {type PropsWithChildren, useContext, useEffect, useRef, useState} from 'react'
4
+ import React, {type PropsWithChildren, useEffect, useRef} from 'react'
6
5
  import {defer, of} from 'rxjs'
7
6
  import {catchError} from 'rxjs/operators'
8
7
 
9
- import {getDashboardModuleContext} from '../dashboard/module'
10
8
  import {useAuthState} from '../hooks/auth/useAuthState'
11
9
  import {useSanityInstance} from '../hooks/context/useSanityInstance'
10
+ import {useMessageBus} from '../hooks/dashboard/useMessageBus'
12
11
 
13
12
  /**
14
13
  * Keeps the SDK auth token in sync with the dashboard "OS".
@@ -95,15 +94,10 @@ function DashboardTokenRefresh({
95
94
  * @category Dashboard
96
95
  */
97
96
  export const DashboardTokenRefreshProvider: React.FC<PropsWithChildren> = ({children}) => {
98
- const instance = useSanityInstance()
99
- const moduleId = useContext(getDashboardModuleContext())
100
97
  // The connection is first-caller-wins per instance, and hooks below read it during their
101
98
  // render, before any effect here could run. Connecting in the first render pins the module
102
99
  // identity before they do; nothing has subscribed to the store yet, so the write is safe.
103
- // The module id is read once: the CLI wrapper provides it statically above this tree.
104
- // No retry: the host installs the bus at module evaluation, before any remote renders, and
105
- // a standalone app has no host to wait for.
106
- const [messageBus] = useState(() => getDashboardMessageBus(instance, moduleId))
100
+ const messageBus = useMessageBus()
107
101
  if (messageBus) {
108
102
  return <DashboardTokenRefresh messageBus={messageBus}>{children}</DashboardTokenRefresh>
109
103
  }
@@ -1,17 +1,10 @@
1
1
  import {afterEach, expect, it} from 'vitest'
2
2
 
3
3
  import {contextLikeTheCliWrapper} from '../../test/dashboardSlots'
4
- import {getDashboardModuleContext, getDashboardRootContext} from './module'
4
+ import {getDashboardRootContext} from './module'
5
5
 
6
6
  afterEach(() => {
7
- const globals = globalThis as Record<symbol, unknown>
8
- delete globals[Symbol.for('sanity.os.module')]
9
- delete globals[Symbol.for('sanity.os.root')]
10
- })
11
-
12
- it('reads the module context the CLI wrapper created', () => {
13
- const context = contextLikeTheCliWrapper('sanity.os.module')
14
- expect(getDashboardModuleContext()).toBe(context)
7
+ delete (globalThis as Record<symbol, unknown>)[Symbol.for('sanity.os.root')]
15
8
  })
16
9
 
17
10
  it('reads the root context the CLI wrapper created', () => {
@@ -1,62 +1,46 @@
1
1
  import * as React from 'react'
2
2
 
3
- const MODULE_SLOT_KEY = Symbol.for('sanity.os.module')
4
3
  const ROOT_SLOT_KEY = Symbol.for('sanity.os.root')
5
4
 
6
5
  /** The root a CLI-generated wrapper rendered the app into. */
7
6
  interface DashboardRoot {
8
7
  /** Aborts when the host disposes the root, never when it hides it. */
9
8
  signal: AbortSignal
9
+ /** The federation module id the host rendered, from `renderOptions.moduleId`. */
10
+ moduleId?: string
11
+ /** The key of the host's bus connection, when the host passed `renderOptions.parentKey`. */
12
+ parentKey?: string
10
13
  }
11
14
 
12
- type ContextSlot<T> = WeakMap<typeof React.createContext, React.Context<T | undefined>>
13
-
14
- function getSlotContext<T>(slotKey: symbol): React.Context<T | undefined> {
15
- const globals = globalThis as Record<symbol, ContextSlot<T> | undefined>
16
- const slot = (globals[slotKey] ??= new WeakMap())
17
- const key = React.createContext
18
- let context = slot.get(key)
19
- if (!context) {
20
- context = React.createContext<T | undefined>(undefined)
21
- slot.set(key, context)
22
- }
23
- return context
24
- }
15
+ type RootSlot = WeakMap<typeof React.createContext, React.Context<DashboardRoot | undefined>>
25
16
 
26
17
  /**
27
- * Returns the React context that carries the current federation module id.
18
+ * Returns the React context that carries the {@link DashboardRoot} the app renders in.
28
19
  *
29
20
  * @remarks
30
- * This is the slot the CLI-generated wrapper populates with
31
- * `renderOptions.moduleId`. The context lives in a per-React-copy slot on
32
- * `globalThis` so the CLI wrapper and the SDK share the same context even
33
- * across module copies. Each React copy gets its own context, since a context
21
+ * The context lives in a per-React-copy slot on `globalThis` so the CLI wrapper and the SDK
22
+ * share it even across module copies. Each React copy gets its own context, since a context
34
23
  * created by one copy is inert in another.
35
24
  *
36
- * The slot is keyed on `React.createContext` rather than the `React` namespace
37
- * object: `import * as React` and `import React from 'react'` can yield
38
- * different wrapper objects for the same React copy under bundler interop,
39
- * whereas the `createContext` function is the same reference under both.
25
+ * The slot is keyed on `React.createContext` rather than the `React` namespace object:
26
+ * `import * as React` and `import React from 'react'` can yield different wrapper objects for
27
+ * the same React copy under bundler interop, whereas the `createContext` function is the same
28
+ * reference under both.
40
29
  *
41
30
  * The provider side lives in the CLI, which must not import the SDK:
42
- * `packages/@sanity/workbench-cli/src/actions/build/render-remote.ts` in
43
- * `sanity-io/cli` reconstructs this accessor (same symbol, same key) and
44
- * wraps `App` in the context's `Provider`. Nothing in this repo exports it.
45
- * @internal
46
- */
47
- export function getDashboardModuleContext(): React.Context<string | undefined> {
48
- return getSlotContext<string>(MODULE_SLOT_KEY)
49
- }
50
-
51
- /**
52
- * Returns the React context that carries the {@link DashboardRoot} the app renders in.
53
- *
54
- * @remarks
55
- * Shared with the CLI-generated wrapper the same way as
56
- * {@link getDashboardModuleContext}, under `Symbol.for('sanity.os.root')`. It is a
57
- * separate context because older SDKs read the module context as a plain string.
31
+ * `packages/@sanity/workbench-cli/src/actions/build/render-remote.ts` in `sanity-io/cli`
32
+ * reconstructs this accessor (same symbol, same key) and wraps `App` in the context's
33
+ * `Provider`.
58
34
  * @internal
59
35
  */
60
36
  export function getDashboardRootContext(): React.Context<DashboardRoot | undefined> {
61
- return getSlotContext<DashboardRoot>(ROOT_SLOT_KEY)
37
+ const globals = globalThis as {[ROOT_SLOT_KEY]?: RootSlot}
38
+ const slot = (globals[ROOT_SLOT_KEY] ??= new WeakMap())
39
+ const key = React.createContext
40
+ let context = slot.get(key)
41
+ if (!context) {
42
+ context = React.createContext<DashboardRoot | undefined>(undefined)
43
+ slot.set(key, context)
44
+ }
45
+ return context
62
46
  }
@@ -0,0 +1,22 @@
1
+ import {getDashboardMessageBus} from '@sanity/sdk/_internal'
2
+ import {type MessageBusConnection} from '@sanity/sdk/dashboard'
3
+ import {useContext} from 'react'
4
+
5
+ import {getDashboardRootContext} from '../../dashboard/module'
6
+ import {useSanityInstance} from '../context/useSanityInstance'
7
+
8
+ /**
9
+ * Returns the instance's message bus connection, or `undefined` outside a Dashboard. It connects
10
+ * with the module id and parent key the CLI wrapper provides, so a host reads its own `key` here
11
+ * to pass as the `parentKey` of the modules it renders.
12
+ *
13
+ * @remarks
14
+ * Safe to call on every render: the connection is cached on the instance, so each call returns
15
+ * the same one, and only the first call's identity is used.
16
+ * @internal
17
+ */
18
+ export function useMessageBus(): MessageBusConnection | undefined {
19
+ const instance = useSanityInstance()
20
+ const root = useContext(getDashboardRootContext())
21
+ return getDashboardMessageBus(instance, root?.moduleId, root?.parentKey)
22
+ }