@notionhq/custom-blocks 0.0.76 → 0.0.78

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 (109) hide show
  1. package/HOST.md +68 -37
  2. package/bin/src/bridge/manifest.js +3 -3
  3. package/dist/bridge/SandboxBridge.d.ts +6 -12
  4. package/dist/bridge/SandboxBridge.d.ts.map +1 -1
  5. package/dist/bridge/SandboxBridge.js +123 -71
  6. package/dist/bridge/dataSources/resolve.d.ts +3 -4
  7. package/dist/bridge/dataSources/resolve.d.ts.map +1 -1
  8. package/dist/bridge/dataSources/resolve.js +2 -12
  9. package/dist/bridge/hostState.d.ts +3 -2
  10. package/dist/bridge/hostState.d.ts.map +1 -1
  11. package/dist/bridge/loadManifest.d.ts +4 -4
  12. package/dist/bridge/loadManifest.d.ts.map +1 -1
  13. package/dist/bridge/loadManifest.js +2 -2
  14. package/dist/bridge/manifest.d.ts +3 -3
  15. package/dist/bridge/manifest.js +3 -3
  16. package/dist/bridge/messages/{ready.d.ts → connect.d.ts} +13 -11
  17. package/dist/bridge/messages/connect.d.ts.map +1 -0
  18. package/dist/bridge/messages/{ready.js → connect.js} +14 -13
  19. package/dist/bridge/messages/hostToSandbox.d.ts +24 -79
  20. package/dist/bridge/messages/hostToSandbox.d.ts.map +1 -1
  21. package/dist/bridge/messages/hostToSandbox.js +2 -3
  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/queryDataSource.d.ts +3 -7
  29. package/dist/bridge/messages/queryDataSource.d.ts.map +1 -1
  30. package/dist/bridge/messages/queryDataSource.js +3 -7
  31. package/dist/bridge/messages/queryDataSourceResult.d.ts +4 -240
  32. package/dist/bridge/messages/queryDataSourceResult.d.ts.map +1 -1
  33. package/dist/bridge/messages/queryDataSourceResult.js +4 -22
  34. package/dist/bridge/messages/sandboxToHost.d.ts +21 -7
  35. package/dist/bridge/messages/sandboxToHost.d.ts.map +1 -1
  36. package/dist/bridge/messages/sandboxToHost.js +4 -2
  37. package/dist/bridge/sandboxClient.d.ts +2 -4
  38. package/dist/bridge/sandboxClient.d.ts.map +1 -1
  39. package/dist/bridge/sandboxClient.js +2 -5
  40. package/dist/customBlock.d.ts +1 -1
  41. package/dist/customBlock.d.ts.map +1 -1
  42. package/dist/customBlock.js +1 -1
  43. package/dist/host/createCustomBlockHost.d.ts +6 -3
  44. package/dist/host/createCustomBlockHost.d.ts.map +1 -1
  45. package/dist/host/createCustomBlockHost.js +227 -58
  46. package/dist/host/lifecycle/initErrors.d.ts +1 -1
  47. package/dist/host/lifecycle/initErrors.d.ts.map +1 -1
  48. package/dist/host/lifecycle/initErrors.js +25 -7
  49. package/dist/host/lifecycle/types.d.ts +6 -4
  50. package/dist/host/lifecycle/types.d.ts.map +1 -1
  51. package/dist/host/messages/invalidSandboxMessage.js +0 -3
  52. package/dist/host/queries/querySubscriptions.d.ts +10 -10
  53. package/dist/host/queries/querySubscriptions.d.ts.map +1 -1
  54. package/dist/host/queries/querySubscriptions.js +35 -19
  55. package/dist/host/queries/types.d.ts +5 -4
  56. package/dist/host/queries/types.d.ts.map +1 -1
  57. package/dist/host.d.ts +5 -3
  58. package/dist/host.d.ts.map +1 -1
  59. package/dist/host.js +2 -1
  60. package/dist/index.d.ts +2 -2
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +1 -1
  63. package/dist/init.d.ts +7 -7
  64. package/dist/init.d.ts.map +1 -1
  65. package/dist/init.js +70 -50
  66. package/dist/react/index.d.ts +1 -1
  67. package/dist/react/index.d.ts.map +1 -1
  68. package/dist/react/standalonePreview.d.ts.map +1 -1
  69. package/dist/react/standalonePreview.js +2 -0
  70. package/dist/react/useCustomBlockInit.d.ts +5 -6
  71. package/dist/react/useCustomBlockInit.d.ts.map +1 -1
  72. package/dist/react/useCustomBlockInit.js +3 -19
  73. package/dist/react/useRuntimeState.d.ts +4 -3
  74. package/dist/react/useRuntimeState.d.ts.map +1 -1
  75. package/dist/react/useRuntimeState.js +3 -2
  76. package/dist/version.js +1 -1
  77. package/docs/data-sources.md +7 -5
  78. package/docs/errors.md +5 -1
  79. package/docs/lifecycle.md +19 -31
  80. package/docs/manifest.md +1 -1
  81. package/package.json +1 -1
  82. package/src/bridge/SandboxBridge.ts +151 -77
  83. package/src/bridge/dataSources/resolve.ts +3 -14
  84. package/src/bridge/hostState.ts +3 -2
  85. package/src/bridge/loadManifest.ts +6 -6
  86. package/src/bridge/manifest.ts +3 -3
  87. package/src/bridge/messages/{ready.ts → connect.ts} +15 -14
  88. package/src/bridge/messages/hostToSandbox.ts +2 -6
  89. package/src/bridge/messages/init.ts +24 -10
  90. package/src/bridge/messages/initResult.ts +37 -0
  91. package/src/bridge/messages/queryDataSource.ts +3 -7
  92. package/src/bridge/messages/queryDataSourceResult.ts +4 -24
  93. package/src/bridge/messages/sandboxToHost.ts +4 -2
  94. package/src/bridge/sandboxClient.ts +3 -8
  95. package/src/customBlock.ts +2 -2
  96. package/src/host/createCustomBlockHost.ts +269 -64
  97. package/src/host/lifecycle/initErrors.ts +25 -7
  98. package/src/host/lifecycle/types.ts +13 -4
  99. package/src/host/messages/invalidSandboxMessage.ts +0 -3
  100. package/src/host/queries/querySubscriptions.ts +58 -39
  101. package/src/host/queries/types.ts +10 -11
  102. package/src/host.ts +13 -2
  103. package/src/index.ts +3 -1
  104. package/src/init.ts +73 -60
  105. package/src/react/index.ts +0 -1
  106. package/src/react/standalonePreview.ts +2 -0
  107. package/src/react/useCustomBlockInit.ts +6 -27
  108. package/src/react/useRuntimeState.ts +4 -3
  109. package/dist/bridge/messages/ready.d.ts.map +0 -1
@@ -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,7 +28,6 @@ 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"
@@ -37,10 +40,10 @@ import { unreachable } from "../utils.js"
37
40
  import { initErrorForFailureReason } from "./lifecycle/initErrors.js"
38
41
  import { isValidBridgeProtocolVersion } from "./lifecycle/ready.js"
39
42
  import type {
43
+ ConnectSuccessMessage,
40
44
  CustomBlockHostDataSourcesPayload,
41
45
  CustomBlockHostInitialState,
42
46
  InitStatus,
43
- ReadySuccessMessage,
44
47
  } from "./lifecycle/types.js"
45
48
  import { getInvalidSandboxMessageResponse } from "./messages/invalidSandboxMessage.js"
46
49
  import type {
@@ -54,10 +57,12 @@ export type CustomBlockHostOptions = {
54
57
  iframe: HTMLIFrameElement
55
58
  targetOrigin?: string
56
59
  minBridgeProtocolVersion?: number
57
- noReadyTimeoutMs?: number
60
+ noConnectTimeoutMs?: number
61
+ noInitResultTimeoutMs?: number
58
62
  initialState: CustomBlockHostInitialState
59
63
  handlers: CustomBlockHostHandlers
60
- onReady?: (message: ReadySuccessMessage) => void
64
+ onConnect?: (message: ConnectSuccessMessage) => void
65
+ onInitResult?: (message: InitResultMessage) => void
61
66
  onLog?: (direction: CustomBlockHostLogDirection, payload: unknown) => void
62
67
  }
63
68
 
@@ -75,7 +80,8 @@ export type CustomBlockHostHandle = {
75
80
  }) => void
76
81
  }
77
82
 
78
- const DEFAULT_NO_READY_TIMEOUT_MS = 5_000
83
+ const DEFAULT_NO_CONNECT_TIMEOUT_MS = 5_000
84
+ const DEFAULT_NO_INIT_RESULT_TIMEOUT_MS = 5_000
79
85
  const DEFAULT_MIN_BRIDGE_PROTOCOL_VERSION = 2
80
86
 
81
87
  export function createCustomBlockHost(
@@ -85,20 +91,31 @@ export function createCustomBlockHost(
85
91
  iframe,
86
92
  initialState,
87
93
  handlers,
88
- onReady,
94
+ onConnect,
95
+ onInitResult,
89
96
  onLog,
90
97
  targetOrigin = "*",
91
98
  minBridgeProtocolVersion = DEFAULT_MIN_BRIDGE_PROTOCOL_VERSION,
92
- noReadyTimeoutMs = DEFAULT_NO_READY_TIMEOUT_MS,
99
+ noConnectTimeoutMs = DEFAULT_NO_CONNECT_TIMEOUT_MS,
100
+ noInitResultTimeoutMs = DEFAULT_NO_INIT_RESULT_TIMEOUT_MS,
93
101
  } = options
94
102
 
95
103
  let theme = initialState.theme
96
104
  let parent = initialState.parent
97
105
  let page = initialState.page
98
106
  let dataSources = initialState.dataSources
107
+ let manifest = initialState.manifest
99
108
  let currentUser = initialState.currentUser
100
- let initStatus: InitStatus = "pending"
101
- let noReadyTimeoutId: number | undefined
109
+ let initStatus: InitStatus = "waitingForConnect"
110
+ let noConnectTimeoutId: number | undefined
111
+ let noInitResultTimeoutId: number | undefined
112
+ let activeInitializationId: string | undefined
113
+ let connectedSandbox: ConnectSuccessMessage | undefined
114
+ let pendingThemeChange = false
115
+ let pendingParentChange = false
116
+ let pendingPageChange = false
117
+ let pendingDataSourcesChange: CustomBlockHostDataSourcesPayload | undefined
118
+ let pendingCurrentUserChange = false
102
119
  const querySubscriptions = new QuerySubscriptions()
103
120
 
104
121
  function emit(direction: CustomBlockHostLogDirection, payload: unknown) {
@@ -111,83 +128,135 @@ export function createCustomBlockHost(
111
128
  }
112
129
 
113
130
  function postQueryDataSourceResult(args: {
114
- requestId: string
115
- snapshotId: string
131
+ subscriptionId: string
116
132
  response: CustomBlockHostQueryDataSourceResult
117
133
  }) {
118
- // TODO(custom-blocks): Update when bumping bridge protocol version to 3.
119
- // Protocol v2 sandboxes require the legacy optional-error result shape.
120
- const message: QueryDataSourceResultMessage = {
121
- type: "queryDataSourceResult",
122
- requestId: args.requestId,
123
- snapshotId: args.snapshotId,
124
- items: args.response.items,
125
- hasMore: args.response.hasMore ?? false,
126
- error: args.response.error,
127
- }
134
+ const message: QueryDataSourceResultMessage =
135
+ args.response.status === "error"
136
+ ? {
137
+ type: "queryDataSourceResult",
138
+ subscriptionId: args.subscriptionId,
139
+ status: "error",
140
+ error: args.response.error,
141
+ }
142
+ : {
143
+ type: "queryDataSourceResult",
144
+ subscriptionId: args.subscriptionId,
145
+ status: "success",
146
+ items: args.response.items,
147
+ hasMore: args.response.hasMore,
148
+ }
128
149
  post(message)
129
150
  }
130
151
 
131
- function clearNoReadyTimeout() {
132
- if (noReadyTimeoutId !== undefined) {
133
- window.clearTimeout(noReadyTimeoutId)
134
- noReadyTimeoutId = undefined
152
+ function clearNoConnectTimeout() {
153
+ if (noConnectTimeoutId !== undefined) {
154
+ window.clearTimeout(noConnectTimeoutId)
155
+ noConnectTimeoutId = undefined
156
+ }
157
+ }
158
+
159
+ function clearNoInitResultTimeout() {
160
+ if (noInitResultTimeoutId !== undefined) {
161
+ window.clearTimeout(noInitResultTimeoutId)
162
+ noInitResultTimeoutId = undefined
135
163
  }
136
164
  }
137
165
 
138
166
  function resolveDataSourcesPayload(
139
- ready: ReadySuccessMessage,
167
+ connect: ConnectSuccessMessage,
140
168
  ): CustomBlockHostDataSourcesPayload {
141
- return typeof dataSources === "function" ? dataSources(ready) : dataSources
169
+ return typeof dataSources === "function"
170
+ ? dataSources(connect)
171
+ : dataSources
142
172
  }
143
173
 
144
- function sendInit(ready: ReadySuccessMessage): boolean {
145
- if (initStatus !== "pending") {
174
+ function sendInit(connect: ConnectSuccessMessage): boolean {
175
+ if (initStatus !== "waitingForConnect") {
146
176
  return false
147
177
  }
148
- initStatus = "success"
149
- clearNoReadyTimeout()
178
+ onConnect?.(connect)
179
+ manifest = connect.manifest ?? initialState.manifest
180
+ const resolvedDataSources = resolveDataSourcesPayload(connect)
181
+ const bindingErrorCode = getBindingErrorCode(manifest, resolvedDataSources)
182
+ if (bindingErrorCode !== undefined) {
183
+ sendInitError(
184
+ connect.initializationId,
185
+ initErrorForFailureReason(bindingErrorCode),
186
+ )
187
+ return false
188
+ }
189
+ initStatus = "waitingForInitResult"
190
+ activeInitializationId = connect.initializationId
191
+ clearNoConnectTimeout()
150
192
  const message: InitMessage = {
151
193
  type: "init",
194
+ initializationId: connect.initializationId,
152
195
  status: "success",
153
196
  theme,
154
197
  blockId: initialState.blockId,
155
198
  parent,
156
199
  page,
157
- dataSources: resolveDataSourcesPayload(ready),
200
+ manifest,
201
+ dataSources: resolvedDataSources,
158
202
  currentUser,
159
203
  }
160
204
  post(message)
205
+ noInitResultTimeoutId = window.setTimeout(() => {
206
+ if (initStatus !== "waitingForInitResult") {
207
+ return
208
+ }
209
+ initStatus = "error"
210
+ emit("warn", {
211
+ message: "Sandbox did not send initResult",
212
+ initializationId: connect.initializationId,
213
+ })
214
+ }, noInitResultTimeoutMs)
161
215
  return true
162
216
  }
163
217
 
164
- function sendInitError(error: CustomBlockInitErrorInfo) {
165
- if (initStatus !== "pending") {
218
+ function sendInitError(
219
+ initializationId: string,
220
+ error: CustomBlockInitErrorInfo,
221
+ ) {
222
+ if (initStatus !== "waitingForConnect") {
166
223
  return
167
224
  }
168
225
  initStatus = "error"
169
- clearNoReadyTimeout()
226
+ activeInitializationId = initializationId
227
+ clearNoConnectTimeout()
170
228
  post({
171
229
  type: "init",
230
+ initializationId,
172
231
  status: "error",
173
232
  error,
174
233
  })
175
234
  }
176
235
 
177
236
  function onIframeLoad() {
178
- clearNoReadyTimeout()
179
- noReadyTimeoutId = window.setTimeout(() => {
180
- const initError = initErrorForFailureReason("no_ready")
181
- sendInitError(initError)
182
- }, noReadyTimeoutMs)
237
+ clearNoConnectTimeout()
238
+ if (initStatus !== "waitingForConnect") {
239
+ // Only set the timeout if the iframe loaded before the `connect` message was sent.
240
+ return
241
+ }
242
+ noConnectTimeoutId = window.setTimeout(() => {
243
+ if (initStatus !== "waitingForConnect") {
244
+ // Only do anything if the iframe loaded before the `connect` message was sent.
245
+ return
246
+ }
247
+ initStatus = "error"
248
+ emit("warn", initErrorForFailureReason("no_connect"))
249
+ }, noConnectTimeoutMs)
183
250
  }
184
251
 
185
252
  async function handleQuery(message: QueryDataSourceMessage) {
186
- querySubscriptions.track(message)
253
+ const token = querySubscriptions.track(message)
187
254
  const response = await handlers.queryDataSource(message)
255
+ if (!querySubscriptions.isCurrent(message.subscriptionId, token)) {
256
+ return
257
+ }
188
258
  postQueryDataSourceResult({
189
- requestId: message.requestId,
190
- snapshotId: message.snapshotId,
259
+ subscriptionId: message.subscriptionId,
191
260
  response,
192
261
  })
193
262
  }
@@ -257,13 +326,13 @@ export function createCustomBlockHost(
257
326
  post(result)
258
327
  }
259
328
 
260
- function handleReady(message: ReadyMessage) {
329
+ function handleConnect(message: ConnectMessage) {
261
330
  // Verify the bridge protocol version is valid.
262
331
  if (!isValidBridgeProtocolVersion(message.bridgeProtocolVersion)) {
263
332
  const initError = initErrorForFailureReason("invalid_protocol_version", {
264
333
  currentBridgeProtocolVersion: message.bridgeProtocolVersion,
265
334
  })
266
- sendInitError(initError)
335
+ sendInitError(message.initializationId, initError)
267
336
  return
268
337
  }
269
338
  if (message.bridgeProtocolVersion < minBridgeProtocolVersion) {
@@ -274,19 +343,68 @@ export function createCustomBlockHost(
274
343
  minBridgeProtocolVersion,
275
344
  },
276
345
  )
277
- sendInitError(initError)
346
+ sendInitError(message.initializationId, initError)
278
347
  return
279
348
  }
280
349
 
281
- // If the sandbox reported an error during `ready`, return it as an `init` error.
350
+ // If the sandbox reported an error during `connect`, return it as an `init` error.
282
351
  if (message.status === "error") {
283
- sendInitError(message.error)
352
+ sendInitError(message.initializationId, message.error)
284
353
  return
285
354
  }
286
355
 
287
- if (sendInit(message)) {
288
- onReady?.(message)
356
+ if (!sendInit(message)) {
357
+ return
289
358
  }
359
+ connectedSandbox = message
360
+ }
361
+
362
+ /**
363
+ * Sends any pending host state changes to the sandbox. It is possible for these to change
364
+ * between when the host sends `init` and when the sandbox responds with `initResult`. The host
365
+ * does send any pending state changes until hearing back from the sandbox.
366
+ */
367
+ function flushPendingHostStateChanges() {
368
+ if (pendingThemeChange) {
369
+ pendingThemeChange = false
370
+ post({ type: "themeChanged", theme })
371
+ }
372
+ if (pendingParentChange) {
373
+ pendingParentChange = false
374
+ post({ type: "parentChanged", parent })
375
+ }
376
+ if (pendingPageChange) {
377
+ pendingPageChange = false
378
+ post({ type: "pageChanged", page })
379
+ }
380
+ if (pendingDataSourcesChange !== undefined) {
381
+ const nextDataSources = pendingDataSourcesChange
382
+ pendingDataSourcesChange = undefined
383
+ post({ type: "dataSourcesChanged", dataSources: nextDataSources })
384
+ }
385
+ if (pendingCurrentUserChange) {
386
+ pendingCurrentUserChange = false
387
+ post({ type: "currentUserChanged", currentUser })
388
+ }
389
+ }
390
+
391
+ function handleInitResult(message: InitResultMessage) {
392
+ if (
393
+ initStatus !== "waitingForInitResult" ||
394
+ message.initializationId !== activeInitializationId
395
+ ) {
396
+ emit("warn", {
397
+ message: "Ignored stale or unexpected initResult",
398
+ payload: message,
399
+ })
400
+ return
401
+ }
402
+ clearNoInitResultTimeout()
403
+ initStatus = message.status === "success" ? "success" : "error"
404
+ if (initStatus === "success") {
405
+ flushPendingHostStateChanges()
406
+ }
407
+ onInitResult?.(message)
290
408
  }
291
409
 
292
410
  function onInvalidSandboxMessage(
@@ -300,10 +418,7 @@ export function createCustomBlockHost(
300
418
  })
301
419
  const response = getInvalidSandboxMessageResponse(data)
302
420
 
303
- if (initStatus === "pending" && response.incomingType === "ready") {
304
- const initError = initErrorForFailureReason("invalid_ready")
305
- sendInitError(initError)
306
- } else if (response.nack !== undefined) {
421
+ if (response.nack !== undefined) {
307
422
  post(response.nack)
308
423
  }
309
424
  }
@@ -322,9 +437,24 @@ export function createCustomBlockHost(
322
437
  const message = parsed.output
323
438
  emit("in", message)
324
439
 
440
+ if (
441
+ message.type !== "connect" &&
442
+ message.type !== "initResult" &&
443
+ initStatus !== "success"
444
+ ) {
445
+ emit("warn", {
446
+ message: `Ignored ${message.type} before initialization completed`,
447
+ payload: message,
448
+ })
449
+ return
450
+ }
451
+
325
452
  switch (message.type) {
326
- case "ready":
327
- handleReady(message)
453
+ case "connect":
454
+ handleConnect(message)
455
+ return
456
+ case "initResult":
457
+ handleInitResult(message)
328
458
  return
329
459
  case "queryDataSource":
330
460
  void handleQuery(message)
@@ -361,26 +491,62 @@ export function createCustomBlockHost(
361
491
  stop() {
362
492
  window.removeEventListener("message", onMessage)
363
493
  iframe.removeEventListener("load", onIframeLoad)
364
- clearNoReadyTimeout()
494
+ clearNoConnectTimeout()
495
+ clearNoInitResultTimeout()
496
+ querySubscriptions.clear()
365
497
  },
366
498
  post,
367
499
  setTheme(nextTheme) {
368
500
  theme = nextTheme
501
+ if (initStatus !== "success") {
502
+ if (initStatus === "waitingForInitResult") {
503
+ pendingThemeChange = true
504
+ }
505
+ return
506
+ }
369
507
  const message: ThemeChangedMessage = { type: "themeChanged", theme }
370
508
  post(message)
371
509
  },
372
510
  setParent(nextParent) {
373
511
  parent = nextParent
512
+ if (initStatus !== "success") {
513
+ if (initStatus === "waitingForInitResult") {
514
+ pendingParentChange = true
515
+ }
516
+ return
517
+ }
374
518
  const message: ParentChangedMessage = { type: "parentChanged", parent }
375
519
  post(message)
376
520
  },
377
521
  setPage(nextPage) {
378
522
  page = nextPage
523
+ if (initStatus !== "success") {
524
+ if (initStatus === "waitingForInitResult") {
525
+ pendingPageChange = true
526
+ }
527
+ return
528
+ }
379
529
  const message: PageChangedMessage = { type: "pageChanged", page }
380
530
  post(message)
381
531
  },
382
532
  setDataSources(nextDataSources) {
533
+ if (
534
+ connectedSandbox !== undefined &&
535
+ getBindingErrorCode(manifest, nextDataSources) !== undefined
536
+ ) {
537
+ emit("warn", {
538
+ message: "Ignored invalid data source bindings update",
539
+ payload: nextDataSources,
540
+ })
541
+ return
542
+ }
383
543
  dataSources = nextDataSources
544
+ if (initStatus !== "success") {
545
+ if (initStatus === "waitingForInitResult") {
546
+ pendingDataSourcesChange = nextDataSources
547
+ }
548
+ return
549
+ }
384
550
  const message: DataSourcesChangedMessage = {
385
551
  type: "dataSourcesChanged",
386
552
  dataSources: nextDataSources,
@@ -389,22 +555,61 @@ export function createCustomBlockHost(
389
555
  },
390
556
  setCurrentUser(nextCurrentUser) {
391
557
  currentUser = nextCurrentUser
558
+ if (initStatus !== "success") {
559
+ if (initStatus === "waitingForInitResult") {
560
+ pendingCurrentUserChange = true
561
+ }
562
+ return
563
+ }
392
564
  post({ type: "currentUserChanged", currentUser })
393
565
  },
394
566
  refreshQuery({ dataSourceId, response }) {
395
- const refresh = querySubscriptions.resolveRefresh({
567
+ if (initStatus !== "success") {
568
+ return
569
+ }
570
+ const refreshes = querySubscriptions.resolveRefreshes({
396
571
  dataSourceId,
397
572
  response,
398
573
  })
399
- if (refresh.status === "missing") {
574
+ if (refreshes.length === 0) {
400
575
  emit("warn", `No active query exists for data source "${dataSourceId}"`)
401
576
  return
402
577
  }
403
- postQueryDataSourceResult({
404
- requestId: refresh.requestId,
405
- snapshotId: refresh.snapshotId,
406
- response: refresh.response,
407
- })
578
+ for (const refresh of refreshes) {
579
+ postQueryDataSourceResult(refresh)
580
+ }
408
581
  },
409
582
  }
410
583
  }
584
+
585
+ function getBindingErrorCode(
586
+ manifest: CustomBlockManifest,
587
+ dataSources: CustomBlockHostDataSourcesPayload,
588
+ ): CustomBlockInitErrorCode | undefined {
589
+ for (const [dataSourceKey, manifestDataSource] of Object.entries(
590
+ manifest.dataSources,
591
+ )) {
592
+ const binding = dataSources.bindings[dataSourceKey]
593
+ if (
594
+ binding?.collectionPointer === undefined ||
595
+ binding.collectionSchema === undefined
596
+ ) {
597
+ return "missing_data_source_binding"
598
+ }
599
+ for (const [propertyKey, manifestProperty] of Object.entries(
600
+ manifestDataSource.properties ?? {},
601
+ )) {
602
+ const propertyId = binding.propertyIdsByKey?.[propertyKey]
603
+ if (propertyId === undefined) {
604
+ return "missing_property_binding"
605
+ }
606
+ if (
607
+ binding.collectionSchema.propertiesById[propertyId]?.type !==
608
+ manifestProperty.type
609
+ ) {
610
+ return "invalid_property_binding"
611
+ }
612
+ }
613
+ }
614
+ return undefined
615
+ }
@@ -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
  }