@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.
- package/README.md +39 -19
- package/dist/createCustomBlockHost.d.ts +8 -12
- package/dist/createCustomBlockHost.d.ts.map +1 -1
- package/dist/createCustomBlockHost.js +138 -162
- package/dist/index.d.ts +1 -1
- 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 +5 -30
- package/dist/lifecycle/initializationController.d.ts.map +1 -1
- package/dist/lifecycle/initializationController.js +90 -42
- package/dist/lifecycle/types.d.ts +14 -8
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/messages/sandboxMessageRouter.d.ts +2 -2
- package/dist/messages/sandboxMessageRouter.d.ts.map +1 -1
- package/dist/messages/sandboxMessageRouter.js +5 -5
- package/dist/protocol/messages/init.d.ts +1 -1
- package/package.json +1 -1
- package/src/createCustomBlockHost.ts +176 -187
- package/src/index.ts +2 -1
- package/src/lifecycle/initErrors.ts +6 -0
- package/src/lifecycle/initializationController.ts +100 -77
- package/src/lifecycle/types.ts +20 -11
- package/src/messages/sandboxMessageRouter.ts +7 -7
|
@@ -1,57 +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
|
-
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
|
132
|
+
async function prepareAndSendInit(connect: ConnectSuccessMessage) {
|
|
139
133
|
if (initStatus !== "waitingForConnect") {
|
|
140
|
-
return
|
|
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 = "
|
|
136
|
+
initStatus = "preparingInit"
|
|
151
137
|
activeInitializationId = connect.initializationId
|
|
152
138
|
clearNoConnectTimeout()
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
173
|
-
const error = initErrorForFailureReason("no_init_result")
|
|
143
|
+
const error = initErrorForFailureReason("initial_state_timeout")
|
|
174
144
|
warn({
|
|
175
|
-
|
|
145
|
+
...error,
|
|
176
146
|
initializationId: connect.initializationId,
|
|
177
147
|
})
|
|
178
|
-
|
|
179
|
-
},
|
|
180
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
|
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,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
|
-
| "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() !== "
|
|
142
|
+
if (!isInitHandshakeMessage(message) && getInitStatus() !== "initialized") {
|
|
143
143
|
emit("warn", {
|
|
144
144
|
message: `Ignored ${message.type} before initialization completed`,
|
|
145
145
|
payload: message,
|