@notionhq/custom-blocks-host 0.1.31 → 0.2.1
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.
- package/README.md +39 -18
- package/dist/createCustomBlockHost.d.ts +8 -10
- package/dist/createCustomBlockHost.d.ts.map +1 -1
- package/dist/createCustomBlockHost.js +94 -150
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/lifecycle/initErrors.d.ts.map +1 -1
- package/dist/lifecycle/initErrors.js +6 -0
- package/dist/lifecycle/initializationController.d.ts +3 -27
- package/dist/lifecycle/initializationController.d.ts.map +1 -1
- package/dist/lifecycle/initializationController.js +89 -46
- package/dist/lifecycle/types.d.ts +13 -7
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/protocol/messages/init.d.ts +1 -1
- package/package.json +1 -1
- package/src/createCustomBlockHost.ts +118 -172
- package/src/index.ts +23 -2
- package/src/lifecycle/initErrors.ts +6 -0
- package/src/lifecycle/initializationController.ts +97 -78
- package/src/lifecycle/types.ts +19 -10
|
@@ -1,56 +1,34 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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)
|
|
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
|
|
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 = "
|
|
136
|
+
initStatus = "preparingInit"
|
|
155
137
|
activeInitializationId = connect.initializationId
|
|
156
138
|
clearNoConnectTimeout()
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
177
|
-
const error = initErrorForFailureReason("no_init_result")
|
|
143
|
+
const error = initErrorForFailureReason("initial_state_timeout")
|
|
178
144
|
warn({
|
|
179
|
-
|
|
145
|
+
...error,
|
|
180
146
|
initializationId: connect.initializationId,
|
|
181
147
|
})
|
|
182
|
-
|
|
183
|
-
},
|
|
184
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
|
package/src/lifecycle/types.ts
CHANGED
|
@@ -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
|
-
|
|
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>
|