@notionhq/custom-blocks 0.0.77 → 0.0.79

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.
Files changed (100) hide show
  1. package/HOST.md +68 -34
  2. package/README.md +1 -1
  3. package/bin/src/bridge/manifest.js +3 -3
  4. package/dist/bridge/SandboxBridge.d.ts +8 -13
  5. package/dist/bridge/SandboxBridge.d.ts.map +1 -1
  6. package/dist/bridge/SandboxBridge.js +110 -39
  7. package/dist/bridge/dataSources/resolve.d.ts +3 -4
  8. package/dist/bridge/dataSources/resolve.d.ts.map +1 -1
  9. package/dist/bridge/dataSources/resolve.js +2 -12
  10. package/dist/bridge/hostState.d.ts +2 -0
  11. package/dist/bridge/hostState.d.ts.map +1 -1
  12. package/dist/bridge/loadManifest.d.ts +4 -4
  13. package/dist/bridge/loadManifest.d.ts.map +1 -1
  14. package/dist/bridge/loadManifest.js +2 -2
  15. package/dist/bridge/manifest.d.ts +3 -3
  16. package/dist/bridge/manifest.js +3 -3
  17. package/dist/bridge/messages/{ready.d.ts → connect.d.ts} +14 -12
  18. package/dist/bridge/messages/connect.d.ts.map +1 -0
  19. package/dist/bridge/messages/connect.js +38 -0
  20. package/dist/bridge/messages/hostToSandbox.d.ts +21 -0
  21. package/dist/bridge/messages/hostToSandbox.d.ts.map +1 -1
  22. package/dist/bridge/messages/init.d.ts +35 -6
  23. package/dist/bridge/messages/init.d.ts.map +1 -1
  24. package/dist/bridge/messages/init.js +8 -4
  25. package/dist/bridge/messages/initResult.d.ts +29 -0
  26. package/dist/bridge/messages/initResult.d.ts.map +1 -0
  27. package/dist/bridge/messages/initResult.js +25 -0
  28. package/dist/bridge/messages/sandboxToHost.d.ts +21 -6
  29. package/dist/bridge/messages/sandboxToHost.d.ts.map +1 -1
  30. package/dist/bridge/messages/sandboxToHost.js +4 -2
  31. package/dist/bridge/sandboxClient.d.ts +2 -4
  32. package/dist/bridge/sandboxClient.d.ts.map +1 -1
  33. package/dist/bridge/sandboxClient.js +2 -5
  34. package/dist/customBlock.d.ts +1 -1
  35. package/dist/customBlock.d.ts.map +1 -1
  36. package/dist/customBlock.js +1 -1
  37. package/dist/host/createCustomBlockHost.d.ts +7 -4
  38. package/dist/host/createCustomBlockHost.d.ts.map +1 -1
  39. package/dist/host/createCustomBlockHost.js +215 -38
  40. package/dist/host/lifecycle/initErrors.d.ts +1 -1
  41. package/dist/host/lifecycle/initErrors.d.ts.map +1 -1
  42. package/dist/host/lifecycle/initErrors.js +25 -7
  43. package/dist/host/lifecycle/{ready.d.ts → protocolVersion.d.ts} +1 -1
  44. package/dist/host/lifecycle/protocolVersion.d.ts.map +1 -0
  45. package/dist/host/lifecycle/types.d.ts +6 -4
  46. package/dist/host/lifecycle/types.d.ts.map +1 -1
  47. package/dist/host/messages/invalidSandboxMessage.js +0 -3
  48. package/dist/host.d.ts +5 -3
  49. package/dist/host.d.ts.map +1 -1
  50. package/dist/host.js +2 -1
  51. package/dist/index.d.ts +2 -2
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +1 -1
  54. package/dist/init.d.ts +7 -7
  55. package/dist/init.d.ts.map +1 -1
  56. package/dist/init.js +70 -50
  57. package/dist/react/index.d.ts +1 -1
  58. package/dist/react/index.d.ts.map +1 -1
  59. package/dist/react/standalonePreview.d.ts.map +1 -1
  60. package/dist/react/standalonePreview.js +2 -0
  61. package/dist/react/useCustomBlockInit.d.ts +5 -6
  62. package/dist/react/useCustomBlockInit.d.ts.map +1 -1
  63. package/dist/react/useCustomBlockInit.js +3 -19
  64. package/dist/react/useRuntimeState.d.ts +4 -3
  65. package/dist/react/useRuntimeState.d.ts.map +1 -1
  66. package/dist/react/useRuntimeState.js +3 -2
  67. package/dist/version.js +1 -1
  68. package/docs/data-sources.md +10 -8
  69. package/docs/errors.md +5 -1
  70. package/docs/lifecycle.md +19 -31
  71. package/docs/manifest.md +1 -1
  72. package/package.json +1 -1
  73. package/src/bridge/SandboxBridge.ts +138 -44
  74. package/src/bridge/dataSources/resolve.ts +3 -14
  75. package/src/bridge/hostState.ts +2 -0
  76. package/src/bridge/loadManifest.ts +6 -6
  77. package/src/bridge/manifest.ts +3 -3
  78. package/src/bridge/messages/connect.ts +42 -0
  79. package/src/bridge/messages/init.ts +24 -10
  80. package/src/bridge/messages/initResult.ts +37 -0
  81. package/src/bridge/messages/sandboxToHost.ts +4 -2
  82. package/src/bridge/sandboxClient.ts +3 -8
  83. package/src/customBlock.ts +2 -2
  84. package/src/host/createCustomBlockHost.ts +265 -45
  85. package/src/host/lifecycle/initErrors.ts +25 -7
  86. package/src/host/lifecycle/types.ts +13 -4
  87. package/src/host/messages/invalidSandboxMessage.ts +0 -3
  88. package/src/host.ts +13 -2
  89. package/src/index.ts +3 -1
  90. package/src/init.ts +73 -60
  91. package/src/react/index.ts +0 -1
  92. package/src/react/standalonePreview.ts +2 -0
  93. package/src/react/useCustomBlockInit.ts +6 -27
  94. package/src/react/useRuntimeState.ts +4 -3
  95. package/dist/bridge/messages/ready.d.ts.map +0 -1
  96. package/dist/bridge/messages/ready.js +0 -43
  97. package/dist/host/lifecycle/ready.d.ts.map +0 -1
  98. package/src/bridge/messages/ready.ts +0 -48
  99. /package/dist/host/lifecycle/{ready.js → protocolVersion.js} +0 -0
  100. /package/src/host/lifecycle/{ready.ts → protocolVersion.ts} +0 -0
@@ -57,8 +57,8 @@ export const customBlock = {
57
57
  return getInitializedHostState("getPage").page
58
58
  },
59
59
 
60
- getManifest(): CustomBlockManifest | null {
61
- return customBlockHost.getManifest()
60
+ getManifest(): CustomBlockManifest {
61
+ return getInitializedHostState("getManifest").manifest
62
62
  },
63
63
 
64
64
  autoResize,
@@ -1,5 +1,7 @@
1
1
  import * as v from "valibot"
2
2
  import type { NotionDataSourceId } from "../bridge/ids.js"
3
+ import type { CustomBlockManifest } from "../bridge/manifest.js"
4
+ import type { ConnectMessage } from "../bridge/messages/connect.js"
3
5
  import type { CreatePageMessage } from "../bridge/messages/createPage.js"
4
6
  import type { CreatePageResultMessage } from "../bridge/messages/createPageResult.js"
5
7
  import type { DataSourcesChangedMessage } from "../bridge/messages/dataSourcesChanged.js"
@@ -13,9 +15,11 @@ import type {
13
15
  } from "../bridge/messages/getUser.js"
14
16
  import type { HostToSandboxMessage } from "../bridge/messages/hostToSandbox.js"
15
17
  import type {
18
+ CustomBlockInitErrorCode,
16
19
  CustomBlockInitErrorInfo,
17
20
  InitMessage,
18
21
  } from "../bridge/messages/init.js"
22
+ import type { InitResultMessage } from "../bridge/messages/initResult.js"
19
23
  import type {
20
24
  ListUsersMessage,
21
25
  ListUsersResultMessage,
@@ -24,23 +28,23 @@ import type { PageChangedMessage } from "../bridge/messages/pageChanged.js"
24
28
  import type { ParentChangedMessage } from "../bridge/messages/parentChanged.js"
25
29
  import type { QueryDataSourceMessage } from "../bridge/messages/queryDataSource.js"
26
30
  import type { QueryDataSourceResultMessage } from "../bridge/messages/queryDataSourceResult.js"
27
- import type { ReadyMessage } from "../bridge/messages/ready.js"
28
31
  import { sandboxToHostMessageSchema } from "../bridge/messages/sandboxToHost.js"
29
32
  import type { ThemeChangedMessage } from "../bridge/messages/themeChanged.js"
30
33
  import type { UpdatePageMessage } from "../bridge/messages/updatePage.js"
31
34
  import type { UpdatePageResultMessage } from "../bridge/messages/updatePageResult.js"
32
35
  import type { CustomBlockPage } from "../bridge/pages/page.js"
33
36
  import type { NotionParent } from "../bridge/parent.js"
37
+ import { CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION } from "../bridge/SandboxBridge.js"
34
38
  import type { NotionTheme } from "../bridge/theme.js"
35
39
  import type { NotionUser } from "../bridge/users/user.js"
36
40
  import { unreachable } from "../utils.js"
37
41
  import { initErrorForFailureReason } from "./lifecycle/initErrors.js"
38
- import { isValidBridgeProtocolVersion } from "./lifecycle/ready.js"
42
+ import { isValidBridgeProtocolVersion } from "./lifecycle/protocolVersion.js"
39
43
  import type {
44
+ ConnectSuccessMessage,
40
45
  CustomBlockHostDataSourcesPayload,
41
46
  CustomBlockHostInitialState,
42
47
  InitStatus,
43
- ReadySuccessMessage,
44
48
  } from "./lifecycle/types.js"
45
49
  import { getInvalidSandboxMessageResponse } from "./messages/invalidSandboxMessage.js"
46
50
  import type {
@@ -53,11 +57,13 @@ import type { CustomBlockHostQueryDataSourceResult } from "./queries/types.js"
53
57
  export type CustomBlockHostOptions = {
54
58
  iframe: HTMLIFrameElement
55
59
  targetOrigin?: string
56
- minBridgeProtocolVersion?: number
57
- noReadyTimeoutMs?: number
60
+ minBridgeProtocolVersion: number
61
+ noConnectTimeoutMs?: number
62
+ noInitResultTimeoutMs?: number
58
63
  initialState: CustomBlockHostInitialState
59
64
  handlers: CustomBlockHostHandlers
60
- onReady?: (message: ReadySuccessMessage) => void
65
+ onConnect?: (message: ConnectSuccessMessage) => void
66
+ onInitResult?: (message: InitResultMessage) => void
61
67
  onLog?: (direction: CustomBlockHostLogDirection, payload: unknown) => void
62
68
  }
63
69
 
@@ -75,8 +81,11 @@ export type CustomBlockHostHandle = {
75
81
  }) => void
76
82
  }
77
83
 
78
- const DEFAULT_NO_READY_TIMEOUT_MS = 5_000
79
- const DEFAULT_MIN_BRIDGE_PROTOCOL_VERSION = 2
84
+ const DEFAULT_NO_CONNECT_TIMEOUT_MS = 5_000
85
+ const DEFAULT_NO_INIT_RESULT_TIMEOUT_MS = 5_000
86
+ const initializationIdentitySchema = v.object({
87
+ initializationId: v.string(),
88
+ })
80
89
 
81
90
  export function createCustomBlockHost(
82
91
  options: CustomBlockHostOptions,
@@ -85,20 +94,35 @@ export function createCustomBlockHost(
85
94
  iframe,
86
95
  initialState,
87
96
  handlers,
88
- onReady,
97
+ onConnect,
98
+ onInitResult,
89
99
  onLog,
90
100
  targetOrigin = "*",
91
- minBridgeProtocolVersion = DEFAULT_MIN_BRIDGE_PROTOCOL_VERSION,
92
- noReadyTimeoutMs = DEFAULT_NO_READY_TIMEOUT_MS,
101
+ minBridgeProtocolVersion: configuredMinBridgeProtocolVersion,
102
+ noConnectTimeoutMs = DEFAULT_NO_CONNECT_TIMEOUT_MS,
103
+ noInitResultTimeoutMs = DEFAULT_NO_INIT_RESULT_TIMEOUT_MS,
93
104
  } = options
105
+ const minBridgeProtocolVersion = Math.max(
106
+ configuredMinBridgeProtocolVersion ?? CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
107
+ CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
108
+ )
94
109
 
95
110
  let theme = initialState.theme
96
111
  let parent = initialState.parent
97
112
  let page = initialState.page
98
113
  let dataSources = initialState.dataSources
114
+ let manifest = initialState.manifest
99
115
  let currentUser = initialState.currentUser
100
- let initStatus: InitStatus = "pending"
101
- let noReadyTimeoutId: number | undefined
116
+ let initStatus: InitStatus = "waitingForConnect"
117
+ let noConnectTimeoutId: number | undefined
118
+ let noInitResultTimeoutId: number | undefined
119
+ let activeInitializationId: string | undefined
120
+ let connectedSandbox: ConnectSuccessMessage | undefined
121
+ let pendingThemeChange = false
122
+ let pendingParentChange = false
123
+ let pendingPageChange = false
124
+ let pendingDataSourcesChange: CustomBlockHostDataSourcesPayload | undefined
125
+ let pendingCurrentUserChange = false
102
126
  const querySubscriptions = new QuerySubscriptions()
103
127
 
104
128
  function emit(direction: CustomBlockHostLogDirection, payload: unknown) {
@@ -132,58 +156,104 @@ export function createCustomBlockHost(
132
156
  post(message)
133
157
  }
134
158
 
135
- function clearNoReadyTimeout() {
136
- if (noReadyTimeoutId !== undefined) {
137
- window.clearTimeout(noReadyTimeoutId)
138
- noReadyTimeoutId = undefined
159
+ function clearNoConnectTimeout() {
160
+ if (noConnectTimeoutId !== undefined) {
161
+ window.clearTimeout(noConnectTimeoutId)
162
+ noConnectTimeoutId = undefined
163
+ }
164
+ }
165
+
166
+ function clearNoInitResultTimeout() {
167
+ if (noInitResultTimeoutId !== undefined) {
168
+ window.clearTimeout(noInitResultTimeoutId)
169
+ noInitResultTimeoutId = undefined
139
170
  }
140
171
  }
141
172
 
142
173
  function resolveDataSourcesPayload(
143
- ready: ReadySuccessMessage,
174
+ connect: ConnectSuccessMessage,
144
175
  ): CustomBlockHostDataSourcesPayload {
145
- return typeof dataSources === "function" ? dataSources(ready) : dataSources
176
+ return typeof dataSources === "function"
177
+ ? dataSources(connect)
178
+ : dataSources
146
179
  }
147
180
 
148
- function sendInit(ready: ReadySuccessMessage): boolean {
149
- if (initStatus !== "pending") {
181
+ function sendInit(connect: ConnectSuccessMessage): boolean {
182
+ if (initStatus !== "waitingForConnect") {
150
183
  return false
151
184
  }
152
- initStatus = "success"
153
- clearNoReadyTimeout()
185
+ onConnect?.(connect)
186
+ manifest = connect.manifest ?? initialState.manifest
187
+ const resolvedDataSources = resolveDataSourcesPayload(connect)
188
+ const bindingErrorCode = getBindingErrorCode(manifest, resolvedDataSources)
189
+ if (bindingErrorCode !== undefined) {
190
+ sendInitError(
191
+ connect.initializationId,
192
+ initErrorForFailureReason(bindingErrorCode),
193
+ )
194
+ return false
195
+ }
196
+ initStatus = "waitingForInitResult"
197
+ activeInitializationId = connect.initializationId
198
+ clearNoConnectTimeout()
154
199
  const message: InitMessage = {
155
200
  type: "init",
201
+ initializationId: connect.initializationId,
156
202
  status: "success",
157
203
  theme,
158
204
  blockId: initialState.blockId,
159
205
  parent,
160
206
  page,
161
- dataSources: resolveDataSourcesPayload(ready),
207
+ manifest,
208
+ dataSources: resolvedDataSources,
162
209
  currentUser,
163
210
  }
164
211
  post(message)
212
+ noInitResultTimeoutId = window.setTimeout(() => {
213
+ if (initStatus !== "waitingForInitResult") {
214
+ return
215
+ }
216
+ initStatus = "error"
217
+ emit("warn", {
218
+ message: "Sandbox did not send initResult",
219
+ initializationId: connect.initializationId,
220
+ })
221
+ }, noInitResultTimeoutMs)
165
222
  return true
166
223
  }
167
224
 
168
- function sendInitError(error: CustomBlockInitErrorInfo) {
169
- if (initStatus !== "pending") {
225
+ function sendInitError(
226
+ initializationId: string,
227
+ error: CustomBlockInitErrorInfo,
228
+ ) {
229
+ if (initStatus !== "waitingForConnect") {
170
230
  return
171
231
  }
172
232
  initStatus = "error"
173
- clearNoReadyTimeout()
233
+ activeInitializationId = initializationId
234
+ clearNoConnectTimeout()
174
235
  post({
175
236
  type: "init",
237
+ initializationId,
176
238
  status: "error",
177
239
  error,
178
240
  })
179
241
  }
180
242
 
181
243
  function onIframeLoad() {
182
- clearNoReadyTimeout()
183
- noReadyTimeoutId = window.setTimeout(() => {
184
- const initError = initErrorForFailureReason("no_ready")
185
- sendInitError(initError)
186
- }, noReadyTimeoutMs)
244
+ clearNoConnectTimeout()
245
+ if (initStatus !== "waitingForConnect") {
246
+ // Only set the timeout if the iframe loaded before the `connect` message was sent.
247
+ return
248
+ }
249
+ noConnectTimeoutId = window.setTimeout(() => {
250
+ if (initStatus !== "waitingForConnect") {
251
+ // Only do anything if the iframe loaded before the `connect` message was sent.
252
+ return
253
+ }
254
+ initStatus = "error"
255
+ emit("warn", initErrorForFailureReason("no_connect"))
256
+ }, noConnectTimeoutMs)
187
257
  }
188
258
 
189
259
  async function handleQuery(message: QueryDataSourceMessage) {
@@ -263,13 +333,13 @@ export function createCustomBlockHost(
263
333
  post(result)
264
334
  }
265
335
 
266
- function handleReady(message: ReadyMessage) {
336
+ function handleConnect(message: ConnectMessage) {
267
337
  // Verify the bridge protocol version is valid.
268
338
  if (!isValidBridgeProtocolVersion(message.bridgeProtocolVersion)) {
269
339
  const initError = initErrorForFailureReason("invalid_protocol_version", {
270
340
  currentBridgeProtocolVersion: message.bridgeProtocolVersion,
271
341
  })
272
- sendInitError(initError)
342
+ sendInitError(message.initializationId, initError)
273
343
  return
274
344
  }
275
345
  if (message.bridgeProtocolVersion < minBridgeProtocolVersion) {
@@ -280,19 +350,68 @@ export function createCustomBlockHost(
280
350
  minBridgeProtocolVersion,
281
351
  },
282
352
  )
283
- sendInitError(initError)
353
+ sendInitError(message.initializationId, initError)
284
354
  return
285
355
  }
286
356
 
287
- // If the sandbox reported an error during `ready`, return it as an `init` error.
357
+ // If the sandbox reported an error during `connect`, return it as an `init` error.
288
358
  if (message.status === "error") {
289
- sendInitError(message.error)
359
+ sendInitError(message.initializationId, message.error)
290
360
  return
291
361
  }
292
362
 
293
- if (sendInit(message)) {
294
- onReady?.(message)
363
+ if (!sendInit(message)) {
364
+ return
365
+ }
366
+ connectedSandbox = message
367
+ }
368
+
369
+ /**
370
+ * Sends any pending host state changes to the sandbox. It is possible for these to change
371
+ * between when the host sends `init` and when the sandbox responds with `initResult`. The host
372
+ * does send any pending state changes until hearing back from the sandbox.
373
+ */
374
+ function flushPendingHostStateChanges() {
375
+ if (pendingThemeChange) {
376
+ pendingThemeChange = false
377
+ post({ type: "themeChanged", theme })
378
+ }
379
+ if (pendingParentChange) {
380
+ pendingParentChange = false
381
+ post({ type: "parentChanged", parent })
382
+ }
383
+ if (pendingPageChange) {
384
+ pendingPageChange = false
385
+ post({ type: "pageChanged", page })
386
+ }
387
+ if (pendingDataSourcesChange !== undefined) {
388
+ const nextDataSources = pendingDataSourcesChange
389
+ pendingDataSourcesChange = undefined
390
+ post({ type: "dataSourcesChanged", dataSources: nextDataSources })
391
+ }
392
+ if (pendingCurrentUserChange) {
393
+ pendingCurrentUserChange = false
394
+ post({ type: "currentUserChanged", currentUser })
395
+ }
396
+ }
397
+
398
+ function handleInitResult(message: InitResultMessage) {
399
+ if (
400
+ initStatus !== "waitingForInitResult" ||
401
+ message.initializationId !== activeInitializationId
402
+ ) {
403
+ emit("warn", {
404
+ message: "Ignored stale or unexpected initResult",
405
+ payload: message,
406
+ })
407
+ return
295
408
  }
409
+ clearNoInitResultTimeout()
410
+ initStatus = message.status === "success" ? "success" : "error"
411
+ if (initStatus === "success") {
412
+ flushPendingHostStateChanges()
413
+ }
414
+ onInitResult?.(message)
296
415
  }
297
416
 
298
417
  function onInvalidSandboxMessage(
@@ -306,9 +425,19 @@ export function createCustomBlockHost(
306
425
  })
307
426
  const response = getInvalidSandboxMessageResponse(data)
308
427
 
309
- if (initStatus === "pending" && response.incomingType === "ready") {
310
- const initError = initErrorForFailureReason("invalid_ready")
311
- sendInitError(initError)
428
+ const parsedIdentity = v.safeParse(initializationIdentitySchema, data)
429
+ const initializationId = parsedIdentity.success
430
+ ? parsedIdentity.output.initializationId
431
+ : undefined
432
+ if (
433
+ initStatus === "waitingForConnect" &&
434
+ response.incomingType === "connect" &&
435
+ initializationId !== undefined
436
+ ) {
437
+ sendInitError(
438
+ initializationId,
439
+ initErrorForFailureReason("invalid_connect"),
440
+ )
312
441
  } else if (response.nack !== undefined) {
313
442
  post(response.nack)
314
443
  }
@@ -328,9 +457,24 @@ export function createCustomBlockHost(
328
457
  const message = parsed.output
329
458
  emit("in", message)
330
459
 
460
+ if (
461
+ message.type !== "connect" &&
462
+ message.type !== "initResult" &&
463
+ initStatus !== "success"
464
+ ) {
465
+ emit("warn", {
466
+ message: `Ignored ${message.type} before initialization completed`,
467
+ payload: message,
468
+ })
469
+ return
470
+ }
471
+
331
472
  switch (message.type) {
332
- case "ready":
333
- handleReady(message)
473
+ case "connect":
474
+ handleConnect(message)
475
+ return
476
+ case "initResult":
477
+ handleInitResult(message)
334
478
  return
335
479
  case "queryDataSource":
336
480
  void handleQuery(message)
@@ -367,27 +511,62 @@ export function createCustomBlockHost(
367
511
  stop() {
368
512
  window.removeEventListener("message", onMessage)
369
513
  iframe.removeEventListener("load", onIframeLoad)
370
- clearNoReadyTimeout()
514
+ clearNoConnectTimeout()
515
+ clearNoInitResultTimeout()
371
516
  querySubscriptions.clear()
372
517
  },
373
518
  post,
374
519
  setTheme(nextTheme) {
375
520
  theme = nextTheme
521
+ if (initStatus !== "success") {
522
+ if (initStatus === "waitingForInitResult") {
523
+ pendingThemeChange = true
524
+ }
525
+ return
526
+ }
376
527
  const message: ThemeChangedMessage = { type: "themeChanged", theme }
377
528
  post(message)
378
529
  },
379
530
  setParent(nextParent) {
380
531
  parent = nextParent
532
+ if (initStatus !== "success") {
533
+ if (initStatus === "waitingForInitResult") {
534
+ pendingParentChange = true
535
+ }
536
+ return
537
+ }
381
538
  const message: ParentChangedMessage = { type: "parentChanged", parent }
382
539
  post(message)
383
540
  },
384
541
  setPage(nextPage) {
385
542
  page = nextPage
543
+ if (initStatus !== "success") {
544
+ if (initStatus === "waitingForInitResult") {
545
+ pendingPageChange = true
546
+ }
547
+ return
548
+ }
386
549
  const message: PageChangedMessage = { type: "pageChanged", page }
387
550
  post(message)
388
551
  },
389
552
  setDataSources(nextDataSources) {
553
+ if (
554
+ connectedSandbox !== undefined &&
555
+ getBindingErrorCode(manifest, nextDataSources) !== undefined
556
+ ) {
557
+ emit("warn", {
558
+ message: "Ignored invalid data source bindings update",
559
+ payload: nextDataSources,
560
+ })
561
+ return
562
+ }
390
563
  dataSources = nextDataSources
564
+ if (initStatus !== "success") {
565
+ if (initStatus === "waitingForInitResult") {
566
+ pendingDataSourcesChange = nextDataSources
567
+ }
568
+ return
569
+ }
391
570
  const message: DataSourcesChangedMessage = {
392
571
  type: "dataSourcesChanged",
393
572
  dataSources: nextDataSources,
@@ -396,9 +575,18 @@ export function createCustomBlockHost(
396
575
  },
397
576
  setCurrentUser(nextCurrentUser) {
398
577
  currentUser = nextCurrentUser
578
+ if (initStatus !== "success") {
579
+ if (initStatus === "waitingForInitResult") {
580
+ pendingCurrentUserChange = true
581
+ }
582
+ return
583
+ }
399
584
  post({ type: "currentUserChanged", currentUser })
400
585
  },
401
586
  refreshQuery({ dataSourceId, response }) {
587
+ if (initStatus !== "success") {
588
+ return
589
+ }
402
590
  const refreshes = querySubscriptions.resolveRefreshes({
403
591
  dataSourceId,
404
592
  response,
@@ -413,3 +601,35 @@ export function createCustomBlockHost(
413
601
  },
414
602
  }
415
603
  }
604
+
605
+ function getBindingErrorCode(
606
+ manifest: CustomBlockManifest,
607
+ dataSources: CustomBlockHostDataSourcesPayload,
608
+ ): CustomBlockInitErrorCode | undefined {
609
+ for (const [dataSourceKey, manifestDataSource] of Object.entries(
610
+ manifest.dataSources,
611
+ )) {
612
+ const binding = dataSources.bindings[dataSourceKey]
613
+ if (
614
+ binding?.collectionPointer === undefined ||
615
+ binding.collectionSchema === undefined
616
+ ) {
617
+ return "missing_data_source_binding"
618
+ }
619
+ for (const [propertyKey, manifestProperty] of Object.entries(
620
+ manifestDataSource.properties ?? {},
621
+ )) {
622
+ const propertyId = binding.propertyIdsByKey?.[propertyKey]
623
+ if (propertyId === undefined) {
624
+ return "missing_property_binding"
625
+ }
626
+ if (
627
+ binding.collectionSchema.propertiesById[propertyId]?.type !==
628
+ manifestProperty.type
629
+ ) {
630
+ return "invalid_property_binding"
631
+ }
632
+ }
633
+ }
634
+ return undefined
635
+ }
@@ -10,7 +10,7 @@ const UNKNOWN_ERROR_MESSAGE =
10
10
  * Returns the error info for a given `init` failure code.
11
11
  *
12
12
  * `init` handshakes are one-shot: after an `init` error, this host instance will not answer another
13
- * `ready` message. `isRetryable` means a fresh iframe / `init` attempt could plausibly succeed
13
+ * `connect` message. `isRetryable` means a fresh iframe / `init` attempt could plausibly succeed
14
14
  * without changing block code or host configuration.
15
15
  */
16
16
  export function initErrorForFailureReason(
@@ -21,23 +21,23 @@ export function initErrorForFailureReason(
21
21
  } = {},
22
22
  ): CustomBlockInitErrorInfo {
23
23
  switch (reason) {
24
- case "no_ready":
24
+ case "no_connect":
25
25
  return {
26
26
  code: reason,
27
- message: "Sandbox did not send a ready message.",
27
+ message: "Sandbox did not send a connect message.",
28
28
  isRetryable: true,
29
29
  }
30
- case "invalid_ready":
30
+ case "invalid_connect":
31
31
  return {
32
32
  code: reason,
33
- message: "Sandbox sent an invalid ready message.",
34
- // The sandbox must be updated to send a valid ready message.
33
+ message: "Sandbox sent an invalid connect message.",
34
+ // The sandbox must be updated to send a valid connect message.
35
35
  isRetryable: false,
36
36
  }
37
37
  case "manifest_unavailable":
38
38
  return {
39
39
  code: reason,
40
- message: "Sandbox could not load its manifest.",
40
+ message: "The custom block manifest is unavailable.",
41
41
  isRetryable: true,
42
42
  }
43
43
  case "manifest_invalid":
@@ -115,6 +115,24 @@ export function initErrorForFailureReason(
115
115
  // a property with a compatible schema.
116
116
  isRetryable: false,
117
117
  }
118
+ case "no_init_result":
119
+ return {
120
+ code: reason,
121
+ message: "Sandbox did not acknowledge initialization.",
122
+ isRetryable: true,
123
+ }
124
+ case "invalid_init_result":
125
+ return {
126
+ code: reason,
127
+ message: "Sandbox sent an invalid initialization result.",
128
+ isRetryable: false,
129
+ }
130
+ case "invalid_init_bindings":
131
+ return {
132
+ code: reason,
133
+ message: "Sandbox rejected the host-provided data source bindings.",
134
+ isRetryable: false,
135
+ }
118
136
  case "not_in_iframe":
119
137
  return {
120
138
  code: reason,
@@ -1,14 +1,22 @@
1
1
  import type { NotionDataSourceBindings } from "../../bridge/dataSources/dataSource.js"
2
2
  import type { NotionBlockId } from "../../bridge/ids.js"
3
- import type { ReadyMessage } from "../../bridge/messages/ready.js"
3
+ import type { CustomBlockManifest } from "../../bridge/manifest.js"
4
+ import type { ConnectMessage } from "../../bridge/messages/connect.js"
4
5
  import type { CustomBlockPage } from "../../bridge/pages/page.js"
5
6
  import type { NotionParent } from "../../bridge/parent.js"
6
7
  import type { NotionTheme } from "../../bridge/theme.js"
7
8
  import type { NotionUser } from "../../bridge/users/user.js"
8
9
 
9
- export type ReadySuccessMessage = Extract<ReadyMessage, { status: "success" }>
10
+ export type ConnectSuccessMessage = Extract<
11
+ ConnectMessage,
12
+ { status: "success" }
13
+ >
10
14
 
11
- export type InitStatus = "pending" | "success" | "error"
15
+ export type InitStatus =
16
+ | "waitingForConnect"
17
+ | "waitingForInitResult"
18
+ | "success"
19
+ | "error"
12
20
 
13
21
  export type CustomBlockHostDataSourcesPayload = {
14
22
  bindings: NotionDataSourceBindings
@@ -16,13 +24,14 @@ export type CustomBlockHostDataSourcesPayload = {
16
24
 
17
25
  export type CustomBlockHostInitialDataSources =
18
26
  | CustomBlockHostDataSourcesPayload
19
- | ((ready: ReadySuccessMessage) => CustomBlockHostDataSourcesPayload)
27
+ | ((connect: ConnectSuccessMessage) => CustomBlockHostDataSourcesPayload)
20
28
 
21
29
  export type CustomBlockHostInitialState = {
22
30
  theme: NotionTheme
23
31
  blockId: NotionBlockId
24
32
  parent: NotionParent
25
33
  page: CustomBlockPage
34
+ manifest: CustomBlockManifest
26
35
  dataSources: CustomBlockHostInitialDataSources
27
36
  currentUser: NotionUser
28
37
  }
@@ -28,9 +28,6 @@ export function getInvalidSandboxMessageResponse(
28
28
  function nackReasonForInvalidSandboxMessage(
29
29
  incomingType: string | undefined,
30
30
  ): string {
31
- if (incomingType === "ready") {
32
- return "Invalid manifest"
33
- }
34
31
  if (incomingType !== undefined) {
35
32
  return `Unsupported sandbox message of type "${incomingType}"`
36
33
  }
package/src/host.ts CHANGED
@@ -38,6 +38,8 @@ export {
38
38
  notionPageIdSchema,
39
39
  } from "./bridge/ids.js"
40
40
  export { readIncomingType } from "./bridge/incomingType.js"
41
+ export type { ConnectMessage } from "./bridge/messages/connect.js"
42
+ export { connectMessageSchema } from "./bridge/messages/connect.js"
41
43
  export type { CreatePageMessage } from "./bridge/messages/createPage.js"
42
44
  export { createPageMessageSchema } from "./bridge/messages/createPage.js"
43
45
  export type {
@@ -90,6 +92,16 @@ export {
90
92
  customBlockInitErrorInfoSchema,
91
93
  initMessageSchema,
92
94
  } from "./bridge/messages/init.js"
95
+ export type {
96
+ CustomBlockInitResultErrorCode,
97
+ CustomBlockInitResultErrorInfo,
98
+ InitResultMessage,
99
+ } from "./bridge/messages/initResult.js"
100
+ export {
101
+ customBlockInitResultErrorCodeSchema,
102
+ customBlockInitResultErrorInfoSchema,
103
+ initResultMessageSchema,
104
+ } from "./bridge/messages/initResult.js"
93
105
  export type { InvalidHostMessage } from "./bridge/messages/invalidHostMessage.js"
94
106
  export { invalidHostMessageSchema } from "./bridge/messages/invalidHostMessage.js"
95
107
  export type { InvalidSandboxMessage } from "./bridge/messages/invalidSandboxMessage.js"
@@ -122,8 +134,6 @@ export {
122
134
  customBlockQueryDataSourceErrorInfoSchema,
123
135
  queryDataSourceResultMessageSchema,
124
136
  } from "./bridge/messages/queryDataSourceResult.js"
125
- export type { ReadyMessage } from "./bridge/messages/ready.js"
126
- export { readyMessageSchema } from "./bridge/messages/ready.js"
127
137
  export type { ResizeMessage } from "./bridge/messages/resize.js"
128
138
  export { resizeMessageSchema } from "./bridge/messages/resize.js"
129
139
  export type { SandboxToHostMessage } from "./bridge/messages/sandboxToHost.js"
@@ -151,6 +161,7 @@ export {
151
161
  createCustomBlockHost,
152
162
  } from "./host/createCustomBlockHost.js"
153
163
  export type {
164
+ ConnectSuccessMessage,
154
165
  CustomBlockHostDataSourcesPayload,
155
166
  CustomBlockHostInitialDataSources,
156
167
  CustomBlockHostInitialState,
package/src/index.ts CHANGED
@@ -79,8 +79,10 @@ export type {
79
79
  export type {
80
80
  CustomBlockInitErrorCode,
81
81
  CustomBlockInitErrorInfo,
82
+ CustomBlockInitializationErrorCode,
83
+ CustomBlockInitializationErrorInfo,
82
84
  } from "./bridge/messages/init.js"
83
- export { CustomBlockInitError } from "./bridge/messages/init.js"
85
+ export { CustomBlockInitializationError } from "./bridge/messages/init.js"
84
86
  export type {
85
87
  CustomBlockListUsersErrorCode,
86
88
  CustomBlockListUsersErrorInfo,