import type { NotionDataSource } from "./bridge/dataSources/dataSource.js" import type { NotionBlockId } from "./bridge/ids.js" import { loadManifest } from "./bridge/loadManifest.js" import { CustomBlockInitError, type CustomBlockInitErrorCode, type InitMessage, } from "./bridge/messages/init.js" import type { CustomBlockPage } from "./bridge/pages/page.js" import type { NotionParent } from "./bridge/parent.js" import { customBlockHost } from "./bridge/sandboxClient.js" import type { NotionTheme } from "./bridge/theme.js" import type { NotionUser } from "./bridge/users/user.js" /** * The payload sent by the host in the `init` message in response to the sandbox's `ready` message. */ export type CustomBlockInitial = { theme: NotionTheme blockId: NotionBlockId parent: NotionParent page: CustomBlockPage currentUser: NotionUser dataSources: NotionDataSource[] } /** * Error thrown when the SDK is loaded in a top-level standalone window with no parent frame. * `postMessage` would just hit the same window and the handshake can never complete. * `` catches this specifically and falls back to a standalone preview with a * warning banner. Direct callers can `instanceof` it to apply their own policy. */ export class NotInIframeError extends Error { constructor(message: string = NOT_IN_IFRAME_MESSAGE) { super(message) this.name = "NotInIframeError" this.code = "not_in_iframe" this.isRetryable = false } code: CustomBlockInitErrorCode isRetryable: boolean } /** * Options for {@link initCustomBlock}. */ export type InitCustomBlockOptions = { /** * How long to wait for the host's `init` response before rejecting with an error. * * @default 15000 - Intentionally longer than Notion's 10s host-dependency watchdog * so host-owned init errors can arrive before the SDK's generic fallback appears. */ timeoutMs?: number } const DEFAULT_INIT_TIMEOUT_MS = 15_000 const NOT_IN_IFRAME_MESSAGE = " only works inside an iframe — use the dev shell or deploy to Notion." let initPromise: Promise | undefined /** * Performs the SDK <-> host handshake: loads `custom_blocks.json`, posts * `ready`, then awaits the host's `init` message. Resolves with that payload. * * Rejects with a `CustomBlockInitError` if the host doesn't respond inside `timeoutMs`. * * Idempotent: subsequent calls return the same promise as the first and ignore any new options. * Mount your React tree (or call any SDK hook / `customBlock.subscribe`) only after the * returned promise resolves. */ export function initCustomBlock( opts: InitCustomBlockOptions = {}, ): Promise { if (initPromise === undefined) { initPromise = (async () => { // Fail fast with a typed error when rendered as a standalone tab and not in a parent frame. // Otherwise, it would eventually hit the timeout, since `postMessage` to `window.parent` // would just hit the same window and never arrive. if (typeof window !== "undefined" && window.parent === window) { throw new NotInIframeError() } // Load the manifest and send it to the host. const manifestResult = await loadManifest() customBlockHost.sendReady(manifestResult) const timeoutMs = opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS let message: InitMessage try { message = await customBlockHost.awaitInit( AbortSignal.timeout(timeoutMs), ) } catch (error) { if (isTimeoutError(error)) { throw new CustomBlockInitError({ code: "init_timeout", message: "Host did not respond to init before the timeout.", isRetryable: true, }) } throw error } if (message.status === "error") { console.error( `[notion-custom-sdk] host reported init error (${message.error.code}): ${message.error.message}`, ) throw new CustomBlockInitError(message.error) } const hostState = customBlockHost.getState() if (hostState.status !== "initialized") { throw new CustomBlockInitError({ code: "context_unavailable", message: "Host block payload is unavailable.", isRetryable: true, }) } return { theme: message.theme, blockId: hostState.blockId, parent: hostState.parent, page: hostState.page, currentUser: message.currentUser, dataSources: hostState.dataSources, } })() } return initPromise } function isTimeoutError(error: unknown): boolean { return ( typeof error === "object" && error !== null && "name" in error && (error.name === "TimeoutError" || error.name === "AbortError") ) }