@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.
@@ -1,12 +1,7 @@
1
- import {
2
- DEFAULT_CONTRAST_MODE,
3
- type NotionContrastMode,
4
- } from "@notionhq/custom-blocks-protocol/contrast.js"
1
+ import type { NotionContrastMode } from "@notionhq/custom-blocks-protocol/contrast.js"
5
2
  import type { NotionCollectionSchema } from "@notionhq/custom-blocks-protocol/dataSources/dataSource.js"
6
3
  import type { NotionDataSourceId } from "@notionhq/custom-blocks-protocol/ids.js"
7
4
  import type { CustomBlockManifest } from "@notionhq/custom-blocks-protocol/manifest.js"
8
- import type { ContrastModeChangedMessage } from "@notionhq/custom-blocks-protocol/messages/contrastModeChanged.js"
9
- import type { DataSourcesChangedMessage } from "@notionhq/custom-blocks-protocol/messages/dataSourcesChanged.js"
10
5
  import type { HostToSandboxMessage } from "@notionhq/custom-blocks-protocol/messages/hostToSandbox.js"
11
6
  import type {
12
7
  CustomBlockInitErrorCode,
@@ -14,15 +9,12 @@ import type {
14
9
  } from "@notionhq/custom-blocks-protocol/messages/init.js"
15
10
  import type { InvalidSandboxMessage } from "@notionhq/custom-blocks-protocol/messages/invalidSandboxMessage.js"
16
11
  import type { BridgeMessagePayload } from "@notionhq/custom-blocks-protocol/messages/messagePayload.js"
17
- import type { PageChangedMessage } from "@notionhq/custom-blocks-protocol/messages/pageChanged.js"
18
- import type { ParentChangedMessage } from "@notionhq/custom-blocks-protocol/messages/parentChanged.js"
19
12
  import type { QueryDataSourceMessage } from "@notionhq/custom-blocks-protocol/messages/queryDataSource.js"
20
13
  import type {
21
14
  CustomBlockQueryDataSourceErrorInfo,
22
15
  QueryDataSourceResultMessage,
23
16
  } from "@notionhq/custom-blocks-protocol/messages/queryDataSourceResult.js"
24
17
  import type { SandboxToHostMessage } from "@notionhq/custom-blocks-protocol/messages/sandboxToHost.js"
25
- import type { ThemeChangedMessage } from "@notionhq/custom-blocks-protocol/messages/themeChanged.js"
26
18
  import type { CustomBlockPage } from "@notionhq/custom-blocks-protocol/pages/page.js"
27
19
  import type { NotionParent } from "@notionhq/custom-blocks-protocol/parent.js"
28
20
  import { LATEST_BRIDGE_PROTOCOL_VERSION } from "@notionhq/custom-blocks-protocol/protocolVersion.js"
@@ -33,7 +25,9 @@ import { createInitializationController } from "./lifecycle/initializationContro
33
25
  import { normalizeHostToSandboxMessage } from "./lifecycle/normalizeHostToSandboxMessage.js"
34
26
  import type {
35
27
  CustomBlockHostDataSourcesPayload,
28
+ CustomBlockHostGetInitialState,
36
29
  CustomBlockHostInitialState,
30
+ CustomBlockHostInitialStateResult,
37
31
  } from "./lifecycle/types.js"
38
32
  import { createSandboxMessageRouter } from "./messages/sandboxMessageRouter.js"
39
33
  import type {
@@ -59,19 +53,15 @@ export type CustomBlockHostOptions = {
59
53
  iframe: HTMLIFrameElement
60
54
  sandboxOrigin: string
61
55
  /**
62
- * Minimum sandbox protocol version to accept. Defaults to the host runtime minimum. Higher
63
- * values retire older SDKs; lower values cannot enable protocol versions the runtime no longer
64
- * supports.
56
+ * Minimum sandbox protocol version to accept. Defaults to the host runtime
57
+ * minimum. Higher values retire older SDKs; lower values cannot enable
58
+ * protocol versions the runtime no longer supports.
65
59
  */
66
60
  minBridgeProtocolVersion?: number
67
61
  noConnectTimeoutMs?: number
68
62
  noInitResultTimeoutMs?: number
69
- initialState: CustomBlockHostInitialState
70
63
  handlers: CustomBlockHostHandlers
71
- /**
72
- * Called as soon as the `manifest` is available after `connect`, before `onInitialized` and
73
- * binding validation.
74
- */
64
+ /** Called as soon as the `manifest` is available after `connect`, before `onInitialized` and binding validation. */
75
65
  onManifest?: (manifest: CustomBlockManifest) => void
76
66
  /** Callback fired when the initialization handshake with the sandbox completed successfully. */
77
67
  onInitialized?: (state: {
@@ -86,6 +76,8 @@ export type CustomBlockHostOptions = {
86
76
  */
87
77
  onFailure?: (error: CustomBlockHostFailure) => void
88
78
  onLog?: (direction: CustomBlockHostLogDirection, payload: unknown) => void
79
+ getInitialState: CustomBlockHostGetInitialState
80
+ initialStateTimeoutMs?: number
89
81
  }
90
82
 
91
83
  export type CustomBlockHostHandle = {
@@ -127,7 +119,7 @@ export function createCustomBlockHost(
127
119
  ): CustomBlockHostHandle {
128
120
  const {
129
121
  iframe,
130
- initialState,
122
+ getInitialState,
131
123
  handlers,
132
124
  onManifest,
133
125
  onInitialized,
@@ -136,31 +128,25 @@ export function createCustomBlockHost(
136
128
  sandboxOrigin,
137
129
  minBridgeProtocolVersion: configuredMinBridgeProtocolVersion,
138
130
  noConnectTimeoutMs,
131
+ initialStateTimeoutMs,
139
132
  noInitResultTimeoutMs,
140
133
  } = options
141
- // Callers can retire older SDKs, but cannot restore support for protocols the runtime no longer
142
- // implements. Omission uses the runtime minimum.
134
+ // Callers can retire older SDKs, but cannot restore support for protocols
135
+ // the runtime no longer implements. Omission uses the runtime minimum.
143
136
  const minBridgeProtocolVersion = Math.max(
144
137
  configuredMinBridgeProtocolVersion ??
145
138
  MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION,
146
139
  MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION,
147
140
  )
148
141
 
149
- let theme = initialState.theme
150
- let contrastMode = initialState.contrastMode ?? DEFAULT_CONTRAST_MODE
151
- let parent = initialState.parent
152
- let page = initialState.page
153
- let dataSources = initialState.dataSources
142
+ type HostStateUpdates = Partial<
143
+ Omit<CustomBlockHostInitialState, "blockId" | "manifest">
144
+ >
145
+ let pendingState: HostStateUpdates = {}
146
+ let hasPreparedInitialState = false
147
+ let manifest: CustomBlockManifest | undefined
154
148
  let activeDataSources: CustomBlockHostDataSourcesPayload | undefined
155
149
  let sandboxBridgeProtocolVersion = LATEST_BRIDGE_PROTOCOL_VERSION
156
- let manifest = initialState.manifest
157
- let currentUser = initialState.currentUser
158
- let pendingThemeChange = false
159
- let pendingContrastModeChange = false
160
- let pendingParentChange = false
161
- let pendingPageChange = false
162
- let pendingDataSourcesChange: CustomBlockHostDataSourcesPayload | undefined
163
- let pendingCurrentUserChange = false
164
150
  const querySubscriptions = new QuerySubscriptions()
165
151
 
166
152
  function emit(direction: CustomBlockHostLogDirection, payload: unknown) {
@@ -171,6 +157,7 @@ export function createCustomBlockHost(
171
157
  const status = initializationController.status
172
158
  switch (status) {
173
159
  case "waitingForConnect":
160
+ case "preparingInit":
174
161
  case "waitingForInitResult":
175
162
  case "initialized":
176
163
  case "error":
@@ -211,6 +198,7 @@ export function createCustomBlockHost(
211
198
  return
212
199
  // Send state updates and handler results only after initialization succeeds.
213
200
  case "waitingForConnect":
201
+ case "preparingInit":
214
202
  case "waitingForInitResult":
215
203
  case "error":
216
204
  case "stopped":
@@ -290,95 +278,69 @@ export function createCustomBlockHost(
290
278
  })
291
279
  }
292
280
 
293
- /**
294
- * Sends any pending host state changes to the sandbox. It is possible for these to change
295
- * between when the host sends `init` and when the sandbox responds with `initResult`. The host
296
- * does not send pending state changes until the sandbox responds.
297
- */
281
+ function updateHostState(update: HostStateUpdates) {
282
+ if (
283
+ initializationController.status === "stopped" ||
284
+ initializationController.status === "error"
285
+ ) {
286
+ return
287
+ }
288
+ Object.assign(pendingState, update)
289
+ flushPendingHostStateChanges()
290
+ }
291
+
298
292
  function flushPendingHostStateChanges() {
299
- if (pendingThemeChange) {
300
- pendingThemeChange = false
301
- sendInitializedMessage({ type: "themeChanged", theme })
293
+ // Keep updates queued until the sandbox acknowledges initialization.
294
+ // Updates received before init is sent are included in its payload instead.
295
+ if (initializationController.status !== "initialized") {
296
+ return
302
297
  }
303
- if (pendingContrastModeChange) {
304
- pendingContrastModeChange = false
305
- sendInitializedMessage({ type: "contrastModeChanged", contrastMode })
298
+ const changes = pendingState
299
+ pendingState = {}
300
+ if (changes.theme !== undefined) {
301
+ sendInitializedMessage({ type: "themeChanged", theme: changes.theme })
302
+ }
303
+ if (changes.contrastMode !== undefined) {
304
+ sendInitializedMessage({
305
+ type: "contrastModeChanged",
306
+ contrastMode: changes.contrastMode,
307
+ })
306
308
  }
307
- if (pendingParentChange) {
308
- pendingParentChange = false
309
- sendInitializedMessage({ type: "parentChanged", parent })
309
+ if (changes.parent !== undefined) {
310
+ sendInitializedMessage({ type: "parentChanged", parent: changes.parent })
310
311
  }
311
- if (pendingPageChange) {
312
- pendingPageChange = false
313
- sendInitializedMessage({ type: "pageChanged", page })
312
+ if (changes.page !== undefined) {
313
+ sendInitializedMessage({ type: "pageChanged", page: changes.page })
314
314
  }
315
- if (pendingDataSourcesChange !== undefined) {
316
- const nextDataSources = pendingDataSourcesChange
317
- pendingDataSourcesChange = undefined
315
+ if (changes.dataSources !== undefined) {
318
316
  sendInitializedMessage({
319
317
  type: "dataSourcesChanged",
320
- dataSources: nextDataSources,
318
+ dataSources: changes.dataSources,
321
319
  })
322
320
  }
323
- if (pendingCurrentUserChange) {
324
- pendingCurrentUserChange = false
325
- sendInitializedMessage({ type: "currentUserChanged", currentUser })
321
+ if (changes.currentUser !== undefined) {
322
+ sendInitializedMessage({
323
+ type: "currentUserChanged",
324
+ currentUser: changes.currentUser,
325
+ })
326
326
  }
327
327
  }
328
328
 
329
329
  const initializationController = createInitializationController({
330
- blockId: initialState.blockId,
331
- getState: () => ({
332
- theme,
333
- contrastMode,
334
- parent,
335
- page,
336
- currentUser,
337
- }),
338
- prepareInit(connect) {
330
+ async prepareInit(connect) {
339
331
  sandboxBridgeProtocolVersion = connect.bridgeProtocolVersion
340
- if (
341
- sandboxBridgeProtocolVersion <
342
- BRIDGE_PROTOCOL_VERSION_INTRODUCING_UNAVAILABLE_PAGE &&
343
- page.type === "unavailable"
344
- ) {
345
- return {
346
- status: "error",
347
- error: initErrorForFailureReason("context_unavailable"),
348
- }
349
- }
350
- manifest = connect.manifest ?? initialState.manifest
351
- if (manifest === undefined) {
352
- return {
353
- status: "error",
354
- error: initErrorForFailureReason("manifest_unavailable"),
355
- }
356
- }
357
- onManifest?.(manifest)
358
- const resolvedDataSources =
359
- typeof dataSources === "function" ? dataSources(connect) : dataSources
360
- const bindingErrorCode = getBindingErrorCode(
361
- manifest,
362
- resolvedDataSources,
363
- )
364
- if (bindingErrorCode !== undefined) {
365
- return {
366
- status: "error",
367
- error: initErrorForFailureReason(bindingErrorCode),
368
- }
369
- }
370
- activeDataSources = resolvedDataSources
371
- return {
372
- status: "success",
373
- manifest,
374
- dataSources: resolvedDataSources,
332
+ const result = await getInitialState(connect)
333
+ if (initializationController.status !== "preparingInit") {
334
+ return result
375
335
  }
336
+ return prepareInitialState(result)
376
337
  },
377
338
  sendInitMessage,
378
339
  warn: payload => emit("warn", payload),
379
340
  flushPendingHostStateChanges,
380
341
  minBridgeProtocolVersion,
381
342
  noConnectTimeoutMs,
343
+ initialStateTimeoutMs,
382
344
  noInitResultTimeoutMs,
383
345
  stopHostWithFailure: stopWithFailure,
384
346
  onInitResult(message) {
@@ -389,6 +351,41 @@ export function createCustomBlockHost(
389
351
  onFailure,
390
352
  })
391
353
 
354
+ function prepareInitialState(
355
+ result: CustomBlockHostInitialStateResult,
356
+ ): CustomBlockHostInitialStateResult {
357
+ if (result.status === "error") {
358
+ return result
359
+ }
360
+ manifest = result.initialState.manifest
361
+ onManifest?.(manifest)
362
+ const initialState = { ...result.initialState, ...pendingState }
363
+ pendingState = {}
364
+ if (
365
+ sandboxBridgeProtocolVersion <
366
+ BRIDGE_PROTOCOL_VERSION_INTRODUCING_UNAVAILABLE_PAGE &&
367
+ initialState.page.type === "unavailable"
368
+ ) {
369
+ return {
370
+ status: "error",
371
+ error: initErrorForFailureReason("context_unavailable"),
372
+ }
373
+ }
374
+ const bindingErrorCode = getBindingErrorCode(
375
+ initialState.manifest,
376
+ initialState.dataSources,
377
+ )
378
+ if (bindingErrorCode !== undefined) {
379
+ return {
380
+ status: "error",
381
+ error: initErrorForFailureReason(bindingErrorCode),
382
+ }
383
+ }
384
+ activeDataSources = initialState.dataSources
385
+ hasPreparedInitialState = true
386
+ return { status: "success", initialState }
387
+ }
388
+
392
389
  const onMessage = createSandboxMessageRouter({
393
390
  iframe,
394
391
  sandboxOrigin,
@@ -429,98 +426,47 @@ export function createCustomBlockHost(
429
426
 
430
427
  return {
431
428
  stop,
432
- setTheme(nextTheme) {
433
- theme = nextTheme
434
- if (initializationController.status !== "initialized") {
435
- if (initializationController.status === "waitingForInitResult") {
436
- pendingThemeChange = true
437
- }
438
- return
439
- }
440
- const message: ThemeChangedMessage = { type: "themeChanged", theme }
441
- sendInitializedMessage(message)
429
+ setTheme(theme) {
430
+ updateHostState({ theme })
442
431
  },
443
- setContrastMode(nextContrastMode) {
444
- contrastMode = nextContrastMode
445
- if (initializationController.status !== "initialized") {
446
- if (initializationController.status === "waitingForInitResult") {
447
- pendingContrastModeChange = true
448
- }
449
- return
450
- }
451
- const message: ContrastModeChangedMessage = {
452
- type: "contrastModeChanged",
453
- contrastMode,
454
- }
455
- sendInitializedMessage(message)
432
+ setContrastMode(contrastMode) {
433
+ updateHostState({ contrastMode })
456
434
  },
457
- setParent(nextParent) {
458
- parent = nextParent
459
- if (initializationController.status !== "initialized") {
460
- if (initializationController.status === "waitingForInitResult") {
461
- pendingParentChange = true
462
- }
463
- return
464
- }
465
- const message: ParentChangedMessage = { type: "parentChanged", parent }
466
- sendInitializedMessage(message)
435
+ setParent(parent) {
436
+ updateHostState({ parent })
467
437
  },
468
- setPage(nextPage) {
469
- page = nextPage
438
+ setPage(page) {
470
439
  if (
471
- nextPage.type === "unavailable" &&
440
+ page.type === "unavailable" &&
472
441
  sandboxBridgeProtocolVersion <
473
442
  BRIDGE_PROTOCOL_VERSION_INTRODUCING_UNAVAILABLE_PAGE &&
474
- (initializationController.status === "waitingForInitResult" ||
443
+ ((initializationController.status === "preparingInit" &&
444
+ hasPreparedInitialState) ||
445
+ initializationController.status === "waitingForInitResult" ||
475
446
  initializationController.status === "initialized")
476
447
  ) {
477
448
  stopWithFailure(initErrorForFailureReason("context_unavailable"))
478
449
  return
479
450
  }
480
- if (initializationController.status !== "initialized") {
481
- if (initializationController.status === "waitingForInitResult") {
482
- pendingPageChange = true
483
- }
484
- return
485
- }
486
- const message: PageChangedMessage = { type: "pageChanged", page }
487
- sendInitializedMessage(message)
451
+ updateHostState({ page })
452
+ },
453
+ setCurrentUser(currentUser) {
454
+ updateHostState({ currentUser })
488
455
  },
489
- setDataSources(nextDataSources) {
456
+ setDataSources(dataSources) {
490
457
  if (
491
458
  initializationController.hasConnectedSandbox &&
492
459
  manifest !== undefined &&
493
- getBindingErrorCode(manifest, nextDataSources) !== undefined
460
+ getBindingErrorCode(manifest, dataSources) !== undefined
494
461
  ) {
495
462
  emit("warn", {
496
463
  message: "Ignored invalid data source bindings update",
497
- payload: nextDataSources,
464
+ payload: dataSources,
498
465
  })
499
466
  return
500
467
  }
501
- dataSources = nextDataSources
502
- activeDataSources = nextDataSources
503
- if (initializationController.status !== "initialized") {
504
- if (initializationController.status === "waitingForInitResult") {
505
- pendingDataSourcesChange = nextDataSources
506
- }
507
- return
508
- }
509
- const message: DataSourcesChangedMessage = {
510
- type: "dataSourcesChanged",
511
- dataSources: nextDataSources,
512
- }
513
- sendInitializedMessage(message)
514
- },
515
- setCurrentUser(nextCurrentUser) {
516
- currentUser = nextCurrentUser
517
- if (initializationController.status !== "initialized") {
518
- if (initializationController.status === "waitingForInitResult") {
519
- pendingCurrentUserChange = true
520
- }
521
- return
522
- }
523
- sendInitializedMessage({ type: "currentUserChanged", currentUser })
468
+ activeDataSources = dataSources
469
+ updateHostState({ dataSources })
524
470
  },
525
471
  refreshQuery({ dataSourceId }) {
526
472
  if (initializationController.status !== "initialized") {
package/src/index.ts CHANGED
@@ -1,15 +1,35 @@
1
+ export type { CustomBlockErrorInfo } from "@notionhq/custom-blocks-protocol/errors.js"
1
2
  export type { CreatePageMessage } from "@notionhq/custom-blocks-protocol/messages/createPage.js"
2
3
  export type { GetPageMessage } from "@notionhq/custom-blocks-protocol/messages/getPage.js"
3
4
  export type { GetUserMessage } from "@notionhq/custom-blocks-protocol/messages/getUser.js"
4
5
  export type { ListUsersMessage } from "@notionhq/custom-blocks-protocol/messages/listUsers.js"
5
6
  export type { OpenPageMessage } from "@notionhq/custom-blocks-protocol/messages/openPage.js"
6
- export type { QueryDataSourceMessage } from "@notionhq/custom-blocks-protocol/messages/queryDataSource.js"
7
+ export type {
8
+ CustomBlockCheckboxFilterOperator,
9
+ CustomBlockContainsFilterOperator,
10
+ CustomBlockCreatedByFilterOperator,
11
+ CustomBlockDateFilterOperator,
12
+ CustomBlockNumberFilterOperator,
13
+ CustomBlockOptionFilterOperator,
14
+ CustomBlockPeopleFilterOperator,
15
+ CustomBlockPersonFilterValue,
16
+ CustomBlockTextFilterOperator,
17
+ QueryDataSourceMessage,
18
+ ResolvedCustomBlockDataSourceFilter,
19
+ ResolvedCustomBlockDataSourcePropertyFilter,
20
+ ResolvedCustomBlockDataSourceSort,
21
+ } from "@notionhq/custom-blocks-protocol/messages/queryDataSource.js"
7
22
  export type { ResizeMessage } from "@notionhq/custom-blocks-protocol/messages/resize.js"
8
23
  export type { UpdatePageMessage } from "@notionhq/custom-blocks-protocol/messages/updatePage.js"
9
24
  export type {
25
+ NotionDateInput,
26
+ NotionFileInput,
27
+ NotionPageCover,
28
+ NotionPageIcon,
10
29
  NotionPagePropertyValue,
11
30
  NotionPagePropertyWriteMap,
12
31
  NotionRichTextItem,
32
+ NotionSelectOptionInput,
13
33
  } from "@notionhq/custom-blocks-protocol/pages/page.js"
14
34
  export {
15
35
  type AutoBindableProperty,
@@ -23,8 +43,9 @@ export {
23
43
  export type {
24
44
  ConnectSuccessMessage,
25
45
  CustomBlockHostDataSourcesPayload,
26
- CustomBlockHostInitialDataSources,
46
+ CustomBlockHostGetInitialState,
27
47
  CustomBlockHostInitialState,
48
+ CustomBlockHostInitialStateResult,
28
49
  } from "./lifecycle/types.js"
29
50
  export type {
30
51
  CreatePageMessageInput,
@@ -90,6 +90,12 @@ export function initErrorForFailureReason(
90
90
  message: "Current user is unavailable.",
91
91
  isRetryable: true,
92
92
  }
93
+ case "initial_state_timeout":
94
+ return {
95
+ code: reason,
96
+ message: "Host did not prepare initial state before the timeout.",
97
+ isRetryable: true,
98
+ }
93
99
  case "missing_data_source_binding":
94
100
  return {
95
101
  code: reason,