@notionhq/custom-blocks-host 0.1.31 → 0.2.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.
@@ -1,56 +1,34 @@
1
- import type { NotionContrastMode } from "@notionhq/custom-blocks-protocol/contrast.js"
2
- import type { NotionBlockId } from "@notionhq/custom-blocks-protocol/ids.js"
3
- import type { CustomBlockManifest } from "@notionhq/custom-blocks-protocol/manifest.js"
1
+ import { DEFAULT_CONTRAST_MODE } from "@notionhq/custom-blocks-protocol/contrast.js"
4
2
  import type { ConnectMessage } from "@notionhq/custom-blocks-protocol/messages/connect.js"
5
3
  import type {
6
4
  CustomBlockInitErrorInfo,
7
5
  InitMessage,
8
6
  } from "@notionhq/custom-blocks-protocol/messages/init.js"
9
7
  import type { InitResultMessage } from "@notionhq/custom-blocks-protocol/messages/initResult.js"
10
- import type { CustomBlockPage } from "@notionhq/custom-blocks-protocol/pages/page.js"
11
- import type { NotionParent } from "@notionhq/custom-blocks-protocol/parent.js"
12
- import type { NotionTheme } from "@notionhq/custom-blocks-protocol/theme.js"
13
- import type { NotionUser } from "@notionhq/custom-blocks-protocol/users/user.js"
14
8
  import type { CustomBlockHostFailure } from "../messages/types.js"
9
+ import { unreachable } from "../utils.js"
15
10
  import { initErrorForFailureReason } from "./initErrors.js"
16
11
  import { isValidBridgeProtocolVersion } from "./protocolVersion.js"
17
12
  import type {
18
13
  ConnectSuccessMessage,
19
- CustomBlockHostDataSourcesPayload,
14
+ CustomBlockHostInitialStateResult,
20
15
  InitStatus,
21
16
  } from "./types.js"
22
17
 
23
18
  const DEFAULT_NO_CONNECT_TIMEOUT_MS = 5_000
19
+ const DEFAULT_INITIAL_STATE_TIMEOUT_MS = 5_000
24
20
  const DEFAULT_NO_INIT_RESULT_TIMEOUT_MS = 5_000
25
21
 
26
- type InitializationState = {
27
- theme: NotionTheme
28
- contrastMode: NotionContrastMode
29
- parent: NotionParent
30
- page: CustomBlockPage
31
- currentUser: NotionUser
32
- }
33
-
34
- type InitializationPreparation =
35
- | {
36
- status: "success"
37
- manifest: CustomBlockManifest
38
- dataSources: CustomBlockHostDataSourcesPayload
39
- }
40
- | {
41
- status: "error"
42
- error: CustomBlockInitErrorInfo
43
- }
44
-
45
22
  export type InitializationControllerOptions = {
46
- blockId: NotionBlockId
47
- getState: () => InitializationState
48
- prepareInit: (connect: ConnectSuccessMessage) => InitializationPreparation
23
+ prepareInit: (
24
+ connect: ConnectSuccessMessage,
25
+ ) => Promise<CustomBlockHostInitialStateResult>
49
26
  sendInitMessage: (message: InitMessage) => void
50
27
  warn: (payload: unknown) => void
51
28
  flushPendingHostStateChanges: () => void
52
29
  minBridgeProtocolVersion: number
53
30
  noConnectTimeoutMs?: number
31
+ initialStateTimeoutMs?: number
54
32
  noInitResultTimeoutMs?: number
55
33
  stopHostWithFailure: (error: CustomBlockHostFailure) => void
56
34
  onInitResult?: (message: InitResultMessage) => void
@@ -79,14 +57,13 @@ export function createInitializationController(
79
57
  options: InitializationControllerOptions,
80
58
  ): InitializationController {
81
59
  const {
82
- blockId,
83
- getState,
84
60
  prepareInit,
85
61
  sendInitMessage,
86
62
  warn,
87
63
  flushPendingHostStateChanges,
88
64
  minBridgeProtocolVersion,
89
65
  noConnectTimeoutMs = DEFAULT_NO_CONNECT_TIMEOUT_MS,
66
+ initialStateTimeoutMs = DEFAULT_INITIAL_STATE_TIMEOUT_MS,
90
67
  noInitResultTimeoutMs = DEFAULT_NO_INIT_RESULT_TIMEOUT_MS,
91
68
  stopHostWithFailure,
92
69
  onInitResult,
@@ -95,6 +72,7 @@ export function createInitializationController(
95
72
 
96
73
  let initStatus: InitStatus = "waitingForConnect"
97
74
  let noConnectTimeoutId: number | undefined
75
+ let initialStateTimeoutId: number | undefined
98
76
  let noInitResultTimeoutId: number | undefined
99
77
  let activeInitializationId: string | undefined
100
78
  let hasObservedIframeLoad = false
@@ -107,6 +85,13 @@ export function createInitializationController(
107
85
  }
108
86
  }
109
87
 
88
+ function clearInitialStateTimeout() {
89
+ if (initialStateTimeoutId !== undefined) {
90
+ window.clearTimeout(initialStateTimeoutId)
91
+ initialStateTimeoutId = undefined
92
+ }
93
+ }
94
+
110
95
  function clearNoInitResultTimeout() {
111
96
  if (noInitResultTimeoutId !== undefined) {
112
97
  window.clearTimeout(noInitResultTimeoutId)
@@ -119,12 +104,22 @@ export function createInitializationController(
119
104
  error: CustomBlockInitErrorInfo
120
105
  }) {
121
106
  const { initializationId, error } = args
122
- if (initStatus !== "waitingForConnect") {
123
- return
107
+ switch (initStatus) {
108
+ case "waitingForConnect":
109
+ case "preparingInit":
110
+ break
111
+ case "waitingForInitResult":
112
+ case "initialized":
113
+ case "error":
114
+ case "stopped":
115
+ return
116
+ default:
117
+ return unreachable(initStatus)
124
118
  }
125
119
  initStatus = "error"
126
120
  activeInitializationId = initializationId
127
121
  clearNoConnectTimeout()
122
+ clearInitialStateTimeout()
128
123
  sendInitMessage({
129
124
  type: "init",
130
125
  initializationId,
@@ -134,54 +129,79 @@ export function createInitializationController(
134
129
  onFailure?.(error)
135
130
  }
136
131
 
137
- function prepareAndSendInit(connect: ConnectSuccessMessage): boolean {
138
- if (initStatus !== "waitingForConnect") {
139
- return false
140
- }
141
- const preparation = prepareInit(connect)
142
- // The embedder can stop the host when it receives the manifest.
143
- // If that happens, do not continue initialization or start its timer.
132
+ async function prepareAndSendInit(connect: ConnectSuccessMessage) {
144
133
  if (initStatus !== "waitingForConnect") {
145
- return false
146
- }
147
- if (preparation.status === "error") {
148
- sendInitError({
149
- initializationId: connect.initializationId,
150
- error: preparation.error,
151
- })
152
- return false
134
+ return
153
135
  }
154
- initStatus = "waitingForInitResult"
136
+ initStatus = "preparingInit"
155
137
  activeInitializationId = connect.initializationId
156
138
  clearNoConnectTimeout()
157
- const state = getState()
158
- const message: InitMessage = {
159
- type: "init",
160
- initializationId: connect.initializationId,
161
- status: "success",
162
- theme: state.theme,
163
- contrastMode: state.contrastMode,
164
- blockId,
165
- parent: state.parent,
166
- page: state.page,
167
- manifest: preparation.manifest,
168
- dataSources: preparation.dataSources,
169
- currentUser: state.currentUser,
170
- }
171
- sendInitMessage(message)
172
- noInitResultTimeoutId = window.setTimeout(() => {
173
- if (initStatus !== "waitingForInitResult") {
139
+ initialStateTimeoutId = window.setTimeout(() => {
140
+ if (initStatus !== "preparingInit") {
174
141
  return
175
142
  }
176
- initStatus = "error"
177
- const error = initErrorForFailureReason("no_init_result")
143
+ const error = initErrorForFailureReason("initial_state_timeout")
178
144
  warn({
179
- message: "Sandbox did not send initResult",
145
+ ...error,
180
146
  initializationId: connect.initializationId,
181
147
  })
182
- onFailure?.(error)
183
- }, noInitResultTimeoutMs)
184
- return true
148
+ sendInitError({ initializationId: connect.initializationId, error })
149
+ }, initialStateTimeoutMs)
150
+
151
+ try {
152
+ const preparation = await prepareInit(connect)
153
+ if (initStatus !== "preparingInit") {
154
+ return
155
+ }
156
+ clearInitialStateTimeout()
157
+ if (preparation.status === "error") {
158
+ sendInitError({
159
+ initializationId: connect.initializationId,
160
+ error: preparation.error,
161
+ })
162
+ return
163
+ }
164
+ initStatus = "waitingForInitResult"
165
+ const message: InitMessage = {
166
+ type: "init",
167
+ initializationId: connect.initializationId,
168
+ status: "success",
169
+ theme: preparation.initialState.theme,
170
+ contrastMode:
171
+ preparation.initialState.contrastMode ?? DEFAULT_CONTRAST_MODE,
172
+ blockId: preparation.initialState.blockId,
173
+ parent: preparation.initialState.parent,
174
+ page: preparation.initialState.page,
175
+ currentUser: preparation.initialState.currentUser,
176
+ manifest: preparation.initialState.manifest,
177
+ dataSources: preparation.initialState.dataSources,
178
+ }
179
+ sendInitMessage(message)
180
+ noInitResultTimeoutId = window.setTimeout(() => {
181
+ if (initStatus !== "waitingForInitResult") {
182
+ return
183
+ }
184
+ initStatus = "error"
185
+ const error = initErrorForFailureReason("no_init_result")
186
+ onFailure?.(error)
187
+ warn({
188
+ ...error,
189
+ initializationId: connect.initializationId,
190
+ })
191
+ }, noInitResultTimeoutMs)
192
+ } catch (error) {
193
+ if (initStatus !== "preparingInit") {
194
+ return
195
+ }
196
+ warn({
197
+ message: "Failed to prepare custom block initialization",
198
+ error,
199
+ })
200
+ sendInitError({
201
+ initializationId: connect.initializationId,
202
+ error: initErrorForFailureReason("unknown_error"),
203
+ })
204
+ }
185
205
  }
186
206
 
187
207
  function handleConnect(message: ConnectMessage) {
@@ -227,9 +247,8 @@ export function createInitializationController(
227
247
  return
228
248
  }
229
249
 
230
- if (prepareAndSendInit(message)) {
231
- hasConnectedSandbox = true
232
- }
250
+ hasConnectedSandbox = true
251
+ void prepareAndSendInit(message)
233
252
  }
234
253
 
235
254
  function handleInvalidConnect(args: {
@@ -327,11 +346,10 @@ export function createInitializationController(
327
346
  return
328
347
  }
329
348
  clearNoInitResultTimeout()
349
+ initStatus = message.status === "success" ? "initialized" : "error"
330
350
  if (message.status === "success") {
331
- initStatus = "initialized"
332
351
  flushPendingHostStateChanges()
333
352
  } else {
334
- initStatus = "error"
335
353
  onFailure?.(message.error)
336
354
  }
337
355
  onInitResult?.(message)
@@ -340,6 +358,7 @@ export function createInitializationController(
340
358
  function stop() {
341
359
  initStatus = "stopped"
342
360
  clearNoConnectTimeout()
361
+ clearInitialStateTimeout()
343
362
  clearNoInitResultTimeout()
344
363
  }
345
364
 
@@ -3,6 +3,7 @@ import type { NotionDataSourceBindings } from "@notionhq/custom-blocks-protocol/
3
3
  import type { NotionBlockId } from "@notionhq/custom-blocks-protocol/ids.js"
4
4
  import type { CustomBlockManifest } from "@notionhq/custom-blocks-protocol/manifest.js"
5
5
  import type { ConnectMessage } from "@notionhq/custom-blocks-protocol/messages/connect.js"
6
+ import type { CustomBlockInitErrorInfo } from "@notionhq/custom-blocks-protocol/messages/init.js"
6
7
  import type { CustomBlockPage } from "@notionhq/custom-blocks-protocol/pages/page.js"
7
8
  import type { NotionParent } from "@notionhq/custom-blocks-protocol/parent.js"
8
9
  import type { NotionTheme } from "@notionhq/custom-blocks-protocol/theme.js"
@@ -17,6 +18,8 @@ export type ConnectSuccessMessage = Extract<
17
18
  export type InitStatus =
18
19
  /** The host is waiting for the sandbox's first `connect` message. */
19
20
  | "waitingForConnect"
21
+ /** The host is waiting for `getInitialState` to prepare `init`. */
22
+ | "preparingInit"
20
23
  /** The host is waiting for a matching `initResult` after it sends `init`. */
21
24
  | "waitingForInitResult"
22
25
  /** The host is initialized and can send live updates. */
@@ -30,10 +33,6 @@ export type CustomBlockHostDataSourcesPayload = {
30
33
  bindings: NotionDataSourceBindings
31
34
  }
32
35
 
33
- export type CustomBlockHostInitialDataSources =
34
- | CustomBlockHostDataSourcesPayload
35
- | ((connect: ConnectSuccessMessage) => CustomBlockHostDataSourcesPayload)
36
-
37
36
  export type CustomBlockHostInitialState = {
38
37
  theme: NotionTheme
39
38
  /**
@@ -44,11 +43,21 @@ export type CustomBlockHostInitialState = {
44
43
  blockId: NotionBlockId
45
44
  parent: NotionParent
46
45
  page: CustomBlockPage
47
- /**
48
- * The host's persisted manifest, when one exists. If omitted, the host
49
- * requires the sandbox to provide a manifest in `connect`.
50
- */
51
- manifest?: CustomBlockManifest
52
- dataSources: CustomBlockHostInitialDataSources
46
+ manifest: CustomBlockManifest
47
+ dataSources: CustomBlockHostDataSourcesPayload
53
48
  currentUser: NotionUser
54
49
  }
50
+
51
+ export type CustomBlockHostInitialStateResult =
52
+ | {
53
+ status: "success"
54
+ initialState: CustomBlockHostInitialState
55
+ }
56
+ | {
57
+ status: "error"
58
+ error: CustomBlockInitErrorInfo
59
+ }
60
+
61
+ export type CustomBlockHostGetInitialState = (
62
+ connect: ConnectSuccessMessage,
63
+ ) => Promise<CustomBlockHostInitialStateResult>