@notionhq/custom-blocks-host 0.1.30 → 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,57 +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
- import type { HostToSandboxMessage } from "@notionhq/custom-blocks-protocol/messages/hostToSandbox.js"
6
3
  import type {
7
4
  CustomBlockInitErrorInfo,
8
5
  InitMessage,
9
6
  } from "@notionhq/custom-blocks-protocol/messages/init.js"
10
7
  import type { InitResultMessage } from "@notionhq/custom-blocks-protocol/messages/initResult.js"
11
- import type { CustomBlockPage } from "@notionhq/custom-blocks-protocol/pages/page.js"
12
- import type { NotionParent } from "@notionhq/custom-blocks-protocol/parent.js"
13
- import type { NotionTheme } from "@notionhq/custom-blocks-protocol/theme.js"
14
- import type { NotionUser } from "@notionhq/custom-blocks-protocol/users/user.js"
15
8
  import type { CustomBlockHostFailure } from "../messages/types.js"
9
+ import { unreachable } from "../utils.js"
16
10
  import { initErrorForFailureReason } from "./initErrors.js"
17
11
  import { isValidBridgeProtocolVersion } from "./protocolVersion.js"
18
12
  import type {
19
13
  ConnectSuccessMessage,
20
- CustomBlockHostDataSourcesPayload,
14
+ CustomBlockHostInitialStateResult,
21
15
  InitStatus,
22
16
  } from "./types.js"
23
17
 
24
18
  const DEFAULT_NO_CONNECT_TIMEOUT_MS = 5_000
19
+ const DEFAULT_INITIAL_STATE_TIMEOUT_MS = 5_000
25
20
  const DEFAULT_NO_INIT_RESULT_TIMEOUT_MS = 5_000
26
21
 
27
- type InitializationState = {
28
- theme: NotionTheme
29
- contrastMode: NotionContrastMode
30
- parent: NotionParent
31
- page: CustomBlockPage
32
- currentUser: NotionUser
33
- }
34
-
35
- type InitializationPreparation =
36
- | {
37
- status: "success"
38
- manifest: CustomBlockManifest
39
- dataSources: CustomBlockHostDataSourcesPayload
40
- }
41
- | {
42
- status: "error"
43
- error: CustomBlockInitErrorInfo
44
- }
45
-
46
22
  export type InitializationControllerOptions = {
47
- blockId: NotionBlockId
48
- getState: () => InitializationState
49
- prepareInit: (connect: ConnectSuccessMessage) => InitializationPreparation
50
- post: (message: HostToSandboxMessage) => void
23
+ prepareInit: (
24
+ connect: ConnectSuccessMessage,
25
+ ) => Promise<CustomBlockHostInitialStateResult>
26
+ sendInitMessage: (message: InitMessage) => void
51
27
  warn: (payload: unknown) => void
52
28
  flushPendingHostStateChanges: () => void
53
29
  minBridgeProtocolVersion: number
54
30
  noConnectTimeoutMs?: number
31
+ initialStateTimeoutMs?: number
55
32
  noInitResultTimeoutMs?: number
56
33
  stopHostWithFailure: (error: CustomBlockHostFailure) => void
57
34
  onInitResult?: (message: InitResultMessage) => void
@@ -80,14 +57,13 @@ export function createInitializationController(
80
57
  options: InitializationControllerOptions,
81
58
  ): InitializationController {
82
59
  const {
83
- blockId,
84
- getState,
85
60
  prepareInit,
86
- post,
61
+ sendInitMessage,
87
62
  warn,
88
63
  flushPendingHostStateChanges,
89
64
  minBridgeProtocolVersion,
90
65
  noConnectTimeoutMs = DEFAULT_NO_CONNECT_TIMEOUT_MS,
66
+ initialStateTimeoutMs = DEFAULT_INITIAL_STATE_TIMEOUT_MS,
91
67
  noInitResultTimeoutMs = DEFAULT_NO_INIT_RESULT_TIMEOUT_MS,
92
68
  stopHostWithFailure,
93
69
  onInitResult,
@@ -96,6 +72,7 @@ export function createInitializationController(
96
72
 
97
73
  let initStatus: InitStatus = "waitingForConnect"
98
74
  let noConnectTimeoutId: number | undefined
75
+ let initialStateTimeoutId: number | undefined
99
76
  let noInitResultTimeoutId: number | undefined
100
77
  let activeInitializationId: string | undefined
101
78
  let hasObservedIframeLoad = false
@@ -108,6 +85,13 @@ export function createInitializationController(
108
85
  }
109
86
  }
110
87
 
88
+ function clearInitialStateTimeout() {
89
+ if (initialStateTimeoutId !== undefined) {
90
+ window.clearTimeout(initialStateTimeoutId)
91
+ initialStateTimeoutId = undefined
92
+ }
93
+ }
94
+
111
95
  function clearNoInitResultTimeout() {
112
96
  if (noInitResultTimeoutId !== undefined) {
113
97
  window.clearTimeout(noInitResultTimeoutId)
@@ -120,13 +104,23 @@ export function createInitializationController(
120
104
  error: CustomBlockInitErrorInfo
121
105
  }) {
122
106
  const { initializationId, error } = args
123
- if (initStatus !== "waitingForConnect") {
124
- 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)
125
118
  }
126
119
  initStatus = "error"
127
120
  activeInitializationId = initializationId
128
121
  clearNoConnectTimeout()
129
- post({
122
+ clearInitialStateTimeout()
123
+ sendInitMessage({
130
124
  type: "init",
131
125
  initializationId,
132
126
  status: "error",
@@ -135,49 +129,79 @@ export function createInitializationController(
135
129
  onFailure?.(error)
136
130
  }
137
131
 
138
- function sendInit(connect: ConnectSuccessMessage): boolean {
132
+ async function prepareAndSendInit(connect: ConnectSuccessMessage) {
139
133
  if (initStatus !== "waitingForConnect") {
140
- return false
141
- }
142
- const preparation = prepareInit(connect)
143
- if (preparation.status === "error") {
144
- sendInitError({
145
- initializationId: connect.initializationId,
146
- error: preparation.error,
147
- })
148
- return false
134
+ return
149
135
  }
150
- initStatus = "waitingForInitResult"
136
+ initStatus = "preparingInit"
151
137
  activeInitializationId = connect.initializationId
152
138
  clearNoConnectTimeout()
153
- const state = getState()
154
- const message: InitMessage = {
155
- type: "init",
156
- initializationId: connect.initializationId,
157
- status: "success",
158
- theme: state.theme,
159
- contrastMode: state.contrastMode,
160
- blockId,
161
- parent: state.parent,
162
- page: state.page,
163
- manifest: preparation.manifest,
164
- dataSources: preparation.dataSources,
165
- currentUser: state.currentUser,
166
- }
167
- post(message)
168
- noInitResultTimeoutId = window.setTimeout(() => {
169
- if (initStatus !== "waitingForInitResult") {
139
+ initialStateTimeoutId = window.setTimeout(() => {
140
+ if (initStatus !== "preparingInit") {
170
141
  return
171
142
  }
172
- initStatus = "error"
173
- const error = initErrorForFailureReason("no_init_result")
143
+ const error = initErrorForFailureReason("initial_state_timeout")
174
144
  warn({
175
- message: "Sandbox did not send initResult",
145
+ ...error,
176
146
  initializationId: connect.initializationId,
177
147
  })
178
- onFailure?.(error)
179
- }, noInitResultTimeoutMs)
180
- 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
+ }
181
205
  }
182
206
 
183
207
  function handleConnect(message: ConnectMessage) {
@@ -223,9 +247,8 @@ export function createInitializationController(
223
247
  return
224
248
  }
225
249
 
226
- if (sendInit(message)) {
227
- hasConnectedSandbox = true
228
- }
250
+ hasConnectedSandbox = true
251
+ void prepareAndSendInit(message)
229
252
  }
230
253
 
231
254
  function handleInvalidConnect(args: {
@@ -323,11 +346,10 @@ export function createInitializationController(
323
346
  return
324
347
  }
325
348
  clearNoInitResultTimeout()
349
+ initStatus = message.status === "success" ? "initialized" : "error"
326
350
  if (message.status === "success") {
327
- initStatus = "success"
328
351
  flushPendingHostStateChanges()
329
352
  } else {
330
- initStatus = "error"
331
353
  onFailure?.(message.error)
332
354
  }
333
355
  onInitResult?.(message)
@@ -336,6 +358,7 @@ export function createInitializationController(
336
358
  function stop() {
337
359
  initStatus = "stopped"
338
360
  clearNoConnectTimeout()
361
+ clearInitialStateTimeout()
339
362
  clearNoInitResultTimeout()
340
363
  }
341
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,10 +18,12 @@ 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. */
23
- | "success"
26
+ | "initialized"
24
27
  /** The host is in an error state after an initialization failure or timeout. */
25
28
  | "error"
26
29
  /** The host is stopped and no longer sends or accepts sandbox messages. */
@@ -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>
@@ -2,8 +2,8 @@ import type { ConnectMessage } from "@notionhq/custom-blocks-protocol/messages/c
2
2
  import type { CreatePageMessage } from "@notionhq/custom-blocks-protocol/messages/createPage.js"
3
3
  import type { GetPageMessage } from "@notionhq/custom-blocks-protocol/messages/getPage.js"
4
4
  import type { GetUserMessage } from "@notionhq/custom-blocks-protocol/messages/getUser.js"
5
- import type { HostToSandboxMessage } from "@notionhq/custom-blocks-protocol/messages/hostToSandbox.js"
6
5
  import type { InitResultMessage } from "@notionhq/custom-blocks-protocol/messages/initResult.js"
6
+ import type { InvalidSandboxMessage } from "@notionhq/custom-blocks-protocol/messages/invalidSandboxMessage.js"
7
7
  import { isInitHandshakeMessage } from "@notionhq/custom-blocks-protocol/messages/isInitHandshakeMessage.js"
8
8
  import type { ListUsersMessage } from "@notionhq/custom-blocks-protocol/messages/listUsers.js"
9
9
  import type { OpenPageMessage } from "@notionhq/custom-blocks-protocol/messages/openPage.js"
@@ -35,7 +35,7 @@ export type SandboxMessageRouterOptions = {
35
35
  sandboxOrigin: string
36
36
  getInitStatus: () => InitStatus
37
37
  emit: (direction: CustomBlockHostLogDirection, payload: unknown) => void
38
- post: (message: HostToSandboxMessage) => void
38
+ sendNack: (message: InvalidSandboxMessage) => void
39
39
  onInvalidConnect: InitializationController["handleInvalidConnect"]
40
40
  onInvalidInitResult: InitializationController["handleInvalidInitResult"]
41
41
  onConnect: (message: ConnectMessage) => void
@@ -61,7 +61,7 @@ export function createSandboxMessageRouter(
61
61
  sandboxOrigin,
62
62
  getInitStatus,
63
63
  emit,
64
- post,
64
+ sendNack,
65
65
  onInvalidConnect,
66
66
  onInvalidInitResult,
67
67
  onConnect,
@@ -116,9 +116,9 @@ export function createSandboxMessageRouter(
116
116
  })
117
117
  }
118
118
 
119
- // Other messages get NACKed indicating they were invalid and skipped
120
- if (response.nack !== undefined) {
121
- post(response.nack)
119
+ // Other messages get NACKed indicating they were invalid and skipped.
120
+ if (response.nack !== undefined && getInitStatus() !== "stopped") {
121
+ sendNack(response.nack)
122
122
  }
123
123
  }
124
124
 
@@ -139,7 +139,7 @@ export function createSandboxMessageRouter(
139
139
  }
140
140
 
141
141
  const message = parsed.output
142
- if (!isInitHandshakeMessage(message) && getInitStatus() !== "success") {
142
+ if (!isInitHandshakeMessage(message) && getInitStatus() !== "initialized") {
143
143
  emit("warn", {
144
144
  message: `Ignored ${message.type} before initialization completed`,
145
145
  payload: message,