@notionhq/custom-blocks-host 0.2.21 → 0.3.0

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.
@@ -4,7 +4,6 @@ import { notionDataSourceBindingsSchema } from "@notionhq/custom-blocks-protocol
4
4
  import type { NotionDataSourceId } from "@notionhq/custom-blocks-protocol/ids.js"
5
5
  import { notionDataSourceIdSchema } from "@notionhq/custom-blocks-protocol/ids.js"
6
6
  import type { CustomBlockManifest } from "@notionhq/custom-blocks-protocol/manifest.js"
7
- import { manifestSchema } from "@notionhq/custom-blocks-protocol/manifest.js"
8
7
  import {
9
8
  type HostToSandboxMessage,
10
9
  hostToSandboxMessageSchema,
@@ -32,15 +31,18 @@ import {
32
31
  invalidPropertyBindingError,
33
32
  missingDataSourceBindingError,
34
33
  missingPropertyBindingError,
34
+ unknownInitializationError,
35
35
  } from "./lifecycle/initErrors.js"
36
36
  import { createInitializationController } from "./lifecycle/initializationController.js"
37
+ import { loadCustomBlockManifest } from "./lifecycle/loadCustomBlockManifest.js"
38
+ import type { CustomBlockHostManifestOptions } from "./lifecycle/manifestOptions.js"
37
39
  import { normalizeHostToSandboxMessage } from "./lifecycle/normalizeHostToSandboxMessage.js"
38
40
  import type {
39
41
  CustomBlockHostDataSourcesPayload,
40
- CustomBlockHostGetInitialState,
41
42
  CustomBlockHostInitialState,
42
43
  CustomBlockHostInitialStateResult,
43
44
  CustomBlockHostNormalizedDataSourcesPayload,
45
+ CustomBlockHostPrepareInitialState,
44
46
  } from "./lifecycle/types.js"
45
47
  import { customBlockHostInitialStateSchema } from "./lifecycle/types.js"
46
48
  import { createSandboxMessageRouter } from "./messages/sandboxMessageRouter.js"
@@ -63,7 +65,7 @@ import { unreachable } from "./utils.js"
63
65
  // Earlier SDKs cannot represent this context, so the host reports context_unavailable.
64
66
  const BRIDGE_PROTOCOL_VERSION_INTRODUCING_UNAVAILABLE_PAGE = 5
65
67
 
66
- export type CustomBlockHostOptions = {
68
+ export type CustomBlockHostOptions = CustomBlockHostManifestOptions & {
67
69
  iframe: HTMLIFrameElement
68
70
  sandboxOrigin: string
69
71
  /**
@@ -75,8 +77,6 @@ export type CustomBlockHostOptions = {
75
77
  noConnectTimeoutMs?: number
76
78
  noInitResultTimeoutMs?: number
77
79
  handlers: CustomBlockHostHandlers
78
- /** Called as soon as the `manifest` is available after `connect`, before `onInitialized` and binding validation. */
79
- onManifest?: (manifest: CustomBlockManifest) => void
80
80
  /** Callback fired when the initialization handshake with the sandbox completed successfully. */
81
81
  onInitialized?: (state: {
82
82
  manifest: CustomBlockManifest
@@ -92,7 +92,8 @@ export type CustomBlockHostOptions = {
92
92
  /** Releases adapter resources when a query ends, including replacement and terminal errors. */
93
93
  onQueryUnsubscribed: ((subscriptionId: string) => void) | undefined
94
94
  onLog?: (direction: CustomBlockHostLogDirection, payload: unknown) => void
95
- getInitialState: CustomBlockHostGetInitialState
95
+ /** Runs once, as soon as the manifest is validated, independently of connect. */
96
+ prepareInitialState: CustomBlockHostPrepareInitialState
96
97
  initialStateTimeoutMs?: number
97
98
  }
98
99
 
@@ -122,9 +123,8 @@ export function createCustomBlockHost(
122
123
  ): CustomBlockHostHandle {
123
124
  const {
124
125
  iframe,
125
- getInitialState,
126
+ prepareInitialState: prepareAdapterState,
126
127
  handlers,
127
- onManifest,
128
128
  onInitialized,
129
129
  onConnectionFailure,
130
130
  onLog,
@@ -148,6 +148,7 @@ export function createCustomBlockHost(
148
148
  >
149
149
  let pendingState: HostStateUpdates = {}
150
150
  let hasPreparedInitialState = false
151
+ const preparationController = new AbortController()
151
152
  let manifest: CustomBlockManifest | undefined
152
153
  let activeDataSources: CustomBlockHostNormalizedDataSourcesPayload | undefined
153
154
  let sandboxBridgeProtocolVersion = LATEST_BRIDGE_PROTOCOL_VERSION
@@ -343,11 +344,11 @@ export function createCustomBlockHost(
343
344
  const initializationController = createInitializationController({
344
345
  async prepareInit(connect) {
345
346
  sandboxBridgeProtocolVersion = connect.bridgeProtocolVersion
346
- const result = await getInitialState(connect)
347
+ const result = await preparation
347
348
  if (initializationController.status !== "preparingInit") {
348
349
  return result
349
350
  }
350
- return prepareInitialState(result)
351
+ return finalizeInitialState(result)
351
352
  },
352
353
  sendInitMessage,
353
354
  warn: payload => emit("warn", payload),
@@ -362,29 +363,25 @@ export function createCustomBlockHost(
362
363
  onInitialized?.({ manifest, initialHeight: message.initialHeight })
363
364
  }
364
365
  },
365
- onConnectionFailure,
366
+ onConnectionFailure: failure => {
367
+ preparationController.abort()
368
+ onConnectionFailure?.(failure)
369
+ },
366
370
  })
367
371
 
368
- function prepareInitialState(
372
+ function finalizeInitialState(
369
373
  result: CustomBlockHostInitialStateResult,
370
374
  ): CustomBlockHostInitialStateResult {
371
375
  if (result.status === "error") {
372
376
  return result
373
377
  }
374
- manifest = v.parse(manifestSchema, result.initialState.manifest)
375
- onManifest?.(manifest)
376
- const initialStateInput = {
378
+ const initialState = v.parse(customBlockHostInitialStateSchema, {
377
379
  ...result.initialState,
378
380
  ...pendingState,
379
- }
381
+ })
382
+ manifest = initialState.manifest
380
383
  pendingState = {}
381
- const initialState = {
382
- ...v.parse(
383
- v.omit(customBlockHostInitialStateSchema, ["manifest"]),
384
- initialStateInput,
385
- ),
386
- manifest,
387
- }
384
+
388
385
  if (
389
386
  sandboxBridgeProtocolVersion <
390
387
  BRIDGE_PROTOCOL_VERSION_INTRODUCING_UNAVAILABLE_PAGE &&
@@ -410,6 +407,49 @@ export function createCustomBlockHost(
410
407
  return { status: "success", initialState }
411
408
  }
412
409
 
410
+ // Attach rejection handling immediately: preparation may finish before connect.
411
+ const preparation = prepare().catch(
412
+ (error: unknown): CustomBlockHostInitialStateResult => {
413
+ if (!preparationController.signal.aborted) {
414
+ emit("warn", {
415
+ message: "Failed to prepare custom block initialization",
416
+ error,
417
+ })
418
+ }
419
+ return { status: "error", error: unknownInitializationError() }
420
+ },
421
+ )
422
+
423
+ async function prepare(): Promise<CustomBlockHostInitialStateResult> {
424
+ const loaded = await loadCustomBlockManifest({
425
+ ...options,
426
+ sandboxOrigin,
427
+ signal: preparationController.signal,
428
+ onError: error => {
429
+ emit("warn", {
430
+ message: "Failed to load custom block manifest",
431
+ error,
432
+ })
433
+ },
434
+ })
435
+ if (loaded.status === "error") {
436
+ return loaded
437
+ }
438
+ if (preparationController.signal.aborted) {
439
+ return { status: "error", error: unknownInitializationError() }
440
+ }
441
+ const result = await prepareAdapterState({
442
+ manifest: loaded.manifest,
443
+ signal: preparationController.signal,
444
+ })
445
+ return result.status === "error"
446
+ ? result
447
+ : {
448
+ status: "success",
449
+ initialState: { ...result.initialState, manifest: loaded.manifest },
450
+ }
451
+ }
452
+
413
453
  const onMessage = createSandboxMessageRouter({
414
454
  iframe,
415
455
  sandboxOrigin,
@@ -436,6 +476,7 @@ export function createCustomBlockHost(
436
476
  iframe.addEventListener("load", initializationController.onIframeLoad)
437
477
 
438
478
  function stop() {
479
+ preparationController.abort()
439
480
  window.removeEventListener("message", onMessage)
440
481
  iframe.removeEventListener("load", initializationController.onIframeLoad)
441
482
  initializationController.stop()
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export type {
5
5
  export type { NotionPropertySchemasById } from "@notionhq/custom-blocks-protocol/dataSources/propertySchema.js"
6
6
  export type { CustomBlockErrorInfo } from "@notionhq/custom-blocks-protocol/errors.js"
7
7
  export type { NotionPageId } from "@notionhq/custom-blocks-protocol/ids.js"
8
+ export type { CustomBlockManifest } from "@notionhq/custom-blocks-protocol/manifest.js"
8
9
  export type { CreatePageMessage } from "@notionhq/custom-blocks-protocol/messages/createPage.js"
9
10
  export type { GetPageMessage } from "@notionhq/custom-blocks-protocol/messages/getPage.js"
10
11
  export type { GetUserMessage } from "@notionhq/custom-blocks-protocol/messages/getUser.js"
@@ -53,12 +54,14 @@ export {
53
54
  createCustomBlockHost,
54
55
  } from "./createCustomBlockHost.js"
55
56
  export { toNotionPageId } from "./ids.js"
57
+ export type { CustomBlockHostManifestOptions } from "./lifecycle/manifestOptions.js"
56
58
  export type {
57
59
  ConnectSuccessMessage,
58
60
  CustomBlockHostDataSourcesPayload,
59
- CustomBlockHostGetInitialState,
60
61
  CustomBlockHostInitialState,
61
62
  CustomBlockHostInitialStateResult,
63
+ CustomBlockHostPreparedStateResult,
64
+ CustomBlockHostPrepareInitialState,
62
65
  } from "./lifecycle/types.js"
63
66
  export { isLocalCustomBlockUrl } from "./localBlockUrl.js"
64
67
  export type {
@@ -0,0 +1,89 @@
1
+ import {
2
+ type CustomBlockManifest,
3
+ manifestSchema,
4
+ } from "@notionhq/custom-blocks-protocol/manifest.js"
5
+ import type { CustomBlockInitErrorInfo } from "@notionhq/custom-blocks-protocol/messages/init.js"
6
+ import * as v from "valibot"
7
+ import { isLocalCustomBlockUrl } from "../localBlockUrl.js"
8
+ import type { CustomBlockHostManifestOptions } from "./manifestOptions.js"
9
+
10
+ /** Internal to the host: preview requests never carry credentials or follow redirects. */
11
+ export async function loadCustomBlockManifest(
12
+ args: CustomBlockHostManifestOptions & {
13
+ sandboxOrigin: string
14
+ signal: AbortSignal
15
+ onError: (error: unknown) => void
16
+ },
17
+ ): Promise<
18
+ | { status: "success"; manifest: CustomBlockManifest }
19
+ | { status: "error"; error: CustomBlockInitErrorInfo }
20
+ > {
21
+ const controller = new AbortController()
22
+ const abort = () => controller.abort()
23
+ args.signal.addEventListener("abort", abort, { once: true })
24
+ const cancelled = new Promise<never>((_resolve, reject) => {
25
+ controller.signal.addEventListener(
26
+ "abort",
27
+ () => reject(controller.signal.reason),
28
+ { once: true },
29
+ )
30
+ })
31
+ if (args.signal.aborted) {
32
+ controller.abort()
33
+ }
34
+ const timeout =
35
+ args.manifestSource === "local"
36
+ ? window.setTimeout(() => {
37
+ controller.abort(
38
+ new Error("Manifest loading timed out after 5000 ms."),
39
+ )
40
+ }, 5_000)
41
+ : undefined
42
+ try {
43
+ const input = await Promise.race([
44
+ args.manifestSource === "local"
45
+ ? fetchLocalCustomBlockManifest(args.sandboxOrigin, controller.signal)
46
+ : args.getPersistedManifest({ signal: controller.signal }),
47
+ cancelled,
48
+ ])
49
+ if (controller.signal.aborted) {
50
+ throw new Error("Manifest loading was cancelled.")
51
+ }
52
+ return { status: "success", manifest: v.parse(manifestSchema, input) }
53
+ } catch (error) {
54
+ if (!args.signal.aborted) {
55
+ args.onError(error)
56
+ }
57
+ return {
58
+ status: "error",
59
+ error: {
60
+ code: "manifest_unavailable",
61
+ message: "The custom block manifest is unavailable.",
62
+ isRetryable: true,
63
+ },
64
+ }
65
+ } finally {
66
+ window.clearTimeout(timeout)
67
+ args.signal.removeEventListener("abort", abort)
68
+ }
69
+ }
70
+
71
+ async function fetchLocalCustomBlockManifest(
72
+ origin: string,
73
+ signal: AbortSignal,
74
+ ): Promise<unknown> {
75
+ const url = new URL(origin)
76
+ if (!isLocalCustomBlockUrl(url) || url.origin === window.location.origin) {
77
+ throw new Error("Preview requires a separate loopback origin.")
78
+ }
79
+ const response = await fetch(new URL("/manifest", url), {
80
+ credentials: "omit",
81
+ redirect: "error",
82
+ cache: "no-store",
83
+ signal,
84
+ })
85
+ if (!response.ok) {
86
+ throw new Error(`Manifest request failed: HTTP ${response.status}.`)
87
+ }
88
+ return response.json()
89
+ }
@@ -0,0 +1,19 @@
1
+ import type { manifestSchema } from "@notionhq/custom-blocks-protocol/manifest.js"
2
+ import type { InferInput } from "valibot"
3
+
4
+ /** Selects the manifest source used during host initialization. */
5
+ export type CustomBlockHostManifestOptions =
6
+ | {
7
+ /** Calls getPersistedManifest for a saved definition or worker registry manifest. */
8
+ manifestSource: "persisted"
9
+ getPersistedManifest: (args: {
10
+ signal: AbortSignal
11
+ }) => Promise<InferInput<typeof manifestSchema> | undefined>
12
+ }
13
+ | {
14
+ /**
15
+ * Fetches /manifest from the loopback sandboxOrigin. No persisted fallback.
16
+ * Local refers to the manifest server, not the host's environment.
17
+ */
18
+ manifestSource: "local"
19
+ }
@@ -1,7 +1,10 @@
1
1
  import { notionContrastModeSchema } from "@notionhq/custom-blocks-protocol/contrast.js"
2
2
  import { notionDataSourceBindingsSchema } from "@notionhq/custom-blocks-protocol/dataSources/dataSource.js"
3
3
  import { notionBlockIdSchema } from "@notionhq/custom-blocks-protocol/ids.js"
4
- import { manifestSchema } from "@notionhq/custom-blocks-protocol/manifest.js"
4
+ import {
5
+ type CustomBlockManifest,
6
+ manifestSchema,
7
+ } from "@notionhq/custom-blocks-protocol/manifest.js"
5
8
  import type { ConnectMessage } from "@notionhq/custom-blocks-protocol/messages/connect.js"
6
9
  import type { CustomBlockInitErrorInfo } from "@notionhq/custom-blocks-protocol/messages/init.js"
7
10
  import { customBlockPageSchema } from "@notionhq/custom-blocks-protocol/pages/page.js"
@@ -19,7 +22,7 @@ export type ConnectSuccessMessage = Extract<
19
22
  export type InitStatus =
20
23
  /** The host is waiting for the sandbox's first `connect` message. */
21
24
  | "waitingForConnect"
22
- /** The host is waiting for `getInitialState` to prepare `init`. */
25
+ /** The host is waiting for host state preparation to complete `init`. */
23
26
  | "preparingInit"
24
27
  /** The host is waiting for a matching `initResult` after it sends `init`. */
25
28
  | "waitingForInitResult"
@@ -67,6 +70,15 @@ export type CustomBlockHostInitialStateResult =
67
70
  error: CustomBlockInitErrorInfo
68
71
  }
69
72
 
70
- export type CustomBlockHostGetInitialState = (
71
- connect: ConnectSuccessMessage,
72
- ) => Promise<CustomBlockHostInitialStateResult>
73
+ /** Adapter-owned state; the runtime adds its selected manifest to init. */
74
+ export type CustomBlockHostPreparedStateResult =
75
+ | {
76
+ status: "success"
77
+ initialState: Omit<CustomBlockHostInitialState, "manifest">
78
+ }
79
+ | { status: "error"; error: CustomBlockInitErrorInfo }
80
+
81
+ export type CustomBlockHostPrepareInitialState = (args: {
82
+ manifest: CustomBlockManifest
83
+ signal: AbortSignal
84
+ }) => Promise<CustomBlockHostPreparedStateResult>