@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 CHANGED
@@ -47,16 +47,20 @@ import { createCustomBlockHost } from "@notionhq/custom-blocks-host"
47
47
  const host = createCustomBlockHost({
48
48
  iframe,
49
49
  sandboxOrigin: "https://example.com",
50
- initialState: {
51
- theme,
52
- contrastMode,
53
- blockId,
54
- parent,
55
- page,
56
- currentUser,
57
- manifest,
58
- dataSources: { bindings },
59
- },
50
+ minBridgeProtocolVersion: 3,
51
+ getInitialState: async () => ({
52
+ status: "success",
53
+ initialState: {
54
+ theme,
55
+ contrastMode,
56
+ blockId,
57
+ parent,
58
+ page,
59
+ currentUser,
60
+ manifest,
61
+ dataSources: { bindings },
62
+ },
63
+ }),
60
64
  handlers: {
61
65
  queryDataSource: async message => ({
62
66
  status: "success",
@@ -113,7 +117,26 @@ initial page fails initialization with `context_unavailable`. If page context be
113
117
  after initialization, the host stops and reports the same error instead of sending an unsupported
114
118
  `pageChanged` message.
115
119
 
116
- - `onManifest` provides the selected manifest as soon as it is available after `connect`, before
120
+ `getInitialState` runs after the sandbox sends `connect`. It returns a promise for the initial state
121
+ result. The host listens immediately and sends `init` only after `getInitialState` resolves. Await
122
+ record and binding loads inside `getInitialState`.
123
+
124
+ `getInitialState` selects the manifest that the host uses. The host uses this manifest even when
125
+ `connect` supplies another. Production hosts can select persisted definitions. Preview hosts can
126
+ select sandbox manifests. A successful result must include a manifest. If no manifest is available,
127
+ return `manifest_unavailable`.
128
+
129
+ `getInitialState` can return `{ status: "error", error }` to stop initialization. The host converts
130
+ a thrown error or rejected promise to `unknown_error`. If preparation exceeds
131
+ `initialStateTimeoutMs`, the host reports `initial_state_timeout` and stops the handshake.
132
+
133
+ `no_connect` means the host did not receive `connect`. `no_init_result` means the sandbox did not
134
+ acknowledge `init`. The SDK waits for the host without its own timeout.
135
+
136
+ The host discards `getInitialState` results after a timeout or stop, including a stop caused by an
137
+ iframe reload.
138
+
139
+ - `onManifest` provides the selected manifest when `getInitialState` resolves successfully, before
117
140
  binding validation and `onInitialized`. Use it to configure bindings even when initialization
118
141
  cannot yet succeed.
119
142
 
@@ -122,9 +145,6 @@ after initialization, the host stops and reports the same error instead of sendi
122
145
  - `onFailure` reports a handshake failure, timeout, unavailable page context, or iframe reload. It
123
146
  can run after initialization succeeds.
124
147
 
125
- The host uses the manifest from `connect`, or `initialState.manifest` if `connect` omits it. If
126
- neither exists, initialization fails with `manifest_unavailable`.
127
-
128
148
  `queryDataSource` is required. Other request handlers are optional. Every request handler may return
129
149
  its result synchronously or as a promise. Results use a `status` discriminator: successful results
130
150
  carry their response payload, while errors carry `{ code, message, isRetryable }`.
@@ -157,12 +177,13 @@ The runtime validates option names, supported sort types, and the limit of ten u
157
177
  properties before it invokes the handler. Sorts retain their array order. Extra fields and mixed
158
178
  filter types fail protocol validation.
159
179
 
160
- `initialState.dataSources` may also be a function of the successful `connect` message. This lets a
161
- host derive bindings from the sandbox-provided manifest.
180
+ Derive bindings from the successful `connect` message inside `getInitialState` and return them in
181
+ `initialState.dataSources`.
162
182
 
163
183
  Set `sandboxOrigin` to the iframe's exact origin. The runtime targets outbound messages to that
164
- origin and accepts inbound messages only from that iframe and origin. The runtime also accepts
165
- `noConnectTimeoutMs` and `noInitResultTimeoutMs`; both default to five seconds.
184
+ origin and accepts inbound messages only from that iframe and origin. The runtime accepts
185
+ `noConnectTimeoutMs`, `initialStateTimeoutMs`, and `noInitResultTimeoutMs`; all default to five
186
+ seconds.
166
187
 
167
188
  ### Initialization and malformed messages
168
189
 
@@ -215,7 +236,6 @@ The returned handle exposes:
215
236
  - `setDataSources()`
216
237
  - `setCurrentUser()`
217
238
  - `refreshQuery()`
218
- - `post()`
219
239
  - `stop()`
220
240
 
221
241
  State updates are sent after `initResult.success`. Updates made earlier are held until
@@ -1,30 +1,25 @@
1
- import { type NotionContrastMode } from "./protocol/contrast.js";
1
+ import type { NotionContrastMode } from "./protocol/contrast.js";
2
2
  import type { NotionDataSourceId } from "./protocol/ids.js";
3
3
  import type { CustomBlockManifest } from "./protocol/manifest.js";
4
- import type { HostToSandboxMessage } from "./protocol/messages/hostToSandbox.js";
5
4
  import type { CustomBlockPage } from "./protocol/pages/page.js";
6
5
  import type { NotionParent } from "./protocol/parent.js";
7
6
  import type { NotionTheme } from "./protocol/theme.js";
8
7
  import type { NotionUser } from "./protocol/users/user.js";
9
- import type { CustomBlockHostDataSourcesPayload, CustomBlockHostInitialState } from "./lifecycle/types.js";
8
+ import type { CustomBlockHostDataSourcesPayload, CustomBlockHostGetInitialState } from "./lifecycle/types.js";
10
9
  import type { CustomBlockHostFailure, CustomBlockHostHandlers, CustomBlockHostLogDirection } from "./messages/types.js";
11
10
  export type CustomBlockHostOptions = {
12
11
  iframe: HTMLIFrameElement;
13
12
  sandboxOrigin: string;
14
13
  /**
15
- * Minimum sandbox protocol version to accept. Defaults to the host runtime minimum. Higher
16
- * values retire older SDKs; lower values cannot enable protocol versions the runtime no longer
17
- * supports.
14
+ * Minimum sandbox protocol version to accept. Defaults to the host runtime
15
+ * minimum. Higher values retire older SDKs; lower values cannot enable
16
+ * protocol versions the runtime no longer supports.
18
17
  */
19
18
  minBridgeProtocolVersion?: number;
20
19
  noConnectTimeoutMs?: number;
21
20
  noInitResultTimeoutMs?: number;
22
- initialState: CustomBlockHostInitialState;
23
21
  handlers: CustomBlockHostHandlers;
24
- /**
25
- * Called as soon as the `manifest` is available after `connect`, before `onInitialized` and
26
- * binding validation.
27
- */
22
+ /** Called as soon as the `manifest` is available after `connect`, before `onInitialized` and binding validation. */
28
23
  onManifest?: (manifest: CustomBlockManifest) => void;
29
24
  /** Callback fired when the initialization handshake with the sandbox completed successfully. */
30
25
  onInitialized?: (state: {
@@ -39,10 +34,11 @@ export type CustomBlockHostOptions = {
39
34
  */
40
35
  onFailure?: (error: CustomBlockHostFailure) => void;
41
36
  onLog?: (direction: CustomBlockHostLogDirection, payload: unknown) => void;
37
+ getInitialState: CustomBlockHostGetInitialState;
38
+ initialStateTimeoutMs?: number;
42
39
  };
43
40
  export type CustomBlockHostHandle = {
44
41
  stop: () => void;
45
- post: (message: HostToSandboxMessage) => void;
46
42
  setTheme: (theme: NotionTheme) => void;
47
43
  setContrastMode: (contrastMode: NotionContrastMode) => void;
48
44
  setParent: (parent: NotionParent) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"createCustomBlockHost.d.ts","sourceRoot":"","sources":["../src/createCustomBlockHost.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,kBAAkB,EACvB,MAAM,8CAA8C,CAAA;AAErD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAA;AACjF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8CAA8C,CAAA;AAGvF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4DAA4D,CAAA;AAYtG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gDAAgD,CAAA;AACrF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4CAA4C,CAAA;AAE9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2CAA2C,CAAA;AAC5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gDAAgD,CAAA;AAIhF,OAAO,KAAK,EACX,iCAAiC,EACjC,2BAA2B,EAC3B,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EACX,sBAAsB,EACtB,uBAAuB,EACvB,2BAA2B,EAC3B,MAAM,qBAAqB,CAAA;AAc5B,MAAM,MAAM,sBAAsB,GAAG;IACpC,MAAM,EAAE,iBAAiB,CAAA;IACzB,aAAa,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,YAAY,EAAE,2BAA2B,CAAA;IACzC,QAAQ,EAAE,uBAAuB,CAAA;IACjC;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,mBAAmB,KAAK,IAAI,CAAA;IACpD,gGAAgG;IAChG,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE;QACvB,QAAQ,EAAE,mBAAmB,CAAA;QAC7B,aAAa,CAAC,EAAE,MAAM,CAAA;KACtB,KAAK,IAAI,CAAA;IACV;;;;;OAKG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAA;IACnD,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,2BAA2B,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CAC1E,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IACnC,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,IAAI,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAC7C,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACtC,eAAe,EAAE,CAAC,YAAY,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAC3D,SAAS,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAA;IACzC,OAAO,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC,cAAc,EAAE,CAAC,WAAW,EAAE,iCAAiC,KAAK,IAAI,CAAA;IACxE,cAAc,EAAE,CAAC,WAAW,EAAE,UAAU,KAAK,IAAI,CAAA;IACjD,YAAY,EAAE,CAAC,IAAI,EAAE;QAAE,YAAY,EAAE,kBAAkB,CAAA;KAAE,KAAK,IAAI,CAAA;CAClE,CAAA;AAyBD,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,sBAAsB,GAC7B,qBAAqB,CAqXvB"}
1
+ {"version":3,"file":"createCustomBlockHost.d.ts","sourceRoot":"","sources":["../src/createCustomBlockHost.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8CAA8C,CAAA;AAEtF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAA;AACjF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8CAA8C,CAAA;AAcvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gDAAgD,CAAA;AACrF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4CAA4C,CAAA;AAE9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2CAA2C,CAAA;AAC5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gDAAgD,CAAA;AAIhF,OAAO,KAAK,EACX,iCAAiC,EACjC,8BAA8B,EAG9B,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EACX,sBAAsB,EACtB,uBAAuB,EACvB,2BAA2B,EAC3B,MAAM,qBAAqB,CAAA;AAe5B,MAAM,MAAM,sBAAsB,GAAG;IACpC,MAAM,EAAE,iBAAiB,CAAA;IACzB,aAAa,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,EAAE,uBAAuB,CAAA;IACjC,oHAAoH;IACpH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,mBAAmB,KAAK,IAAI,CAAA;IACpD,gGAAgG;IAChG,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE;QACvB,QAAQ,EAAE,mBAAmB,CAAA;QAC7B,aAAa,CAAC,EAAE,MAAM,CAAA;KACtB,KAAK,IAAI,CAAA;IACV;;;;;OAKG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAA;IACnD,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,2BAA2B,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAC1E,eAAe,EAAE,8BAA8B,CAAA;IAC/C,qBAAqB,CAAC,EAAE,MAAM,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IACnC,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACtC,eAAe,EAAE,CAAC,YAAY,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAC3D,SAAS,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAA;IACzC,OAAO,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC,cAAc,EAAE,CAAC,WAAW,EAAE,iCAAiC,KAAK,IAAI,CAAA;IACxE,cAAc,EAAE,CAAC,WAAW,EAAE,UAAU,KAAK,IAAI,CAAA;IACjD,YAAY,EAAE,CAAC,IAAI,EAAE;QAAE,YAAY,EAAE,kBAAkB,CAAA;KAAE,KAAK,IAAI,CAAA;CAClE,CAAA;AAyBD,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,sBAAsB,GAC7B,qBAAqB,CA8WvB"}
@@ -1,4 +1,3 @@
1
- import { DEFAULT_CONTRAST_MODE, } from "./protocol/contrast.js";
2
1
  import { LATEST_BRIDGE_PROTOCOL_VERSION } from "./protocol/protocolVersion.js";
3
2
  import { initErrorForFailureReason } from "./lifecycle/initErrors.js";
4
3
  import { createInitializationController } from "./lifecycle/initializationController.js";
@@ -8,43 +7,68 @@ import { MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION } from "./protocolVersion.js
8
7
  import { QuerySubscriptions, } from "./queries/querySubscriptions.js";
9
8
  import { validateQueryDataSourceFilter } from "./queries/validateQueryDataSourceFilter.js";
10
9
  import { validateQueryDataSourceSorts } from "./queries/validateQueryDataSourceSorts.js";
10
+ import { unreachable } from "./utils.js";
11
11
  // Bridge protocol version that introduced page: { type: "unavailable" }.
12
12
  // Earlier SDKs cannot represent this context, so the host reports context_unavailable.
13
13
  const BRIDGE_PROTOCOL_VERSION_INTRODUCING_UNAVAILABLE_PAGE = 5;
14
14
  export function createCustomBlockHost(options) {
15
- const { iframe, initialState, handlers, onManifest, onInitialized, onFailure, onLog, sandboxOrigin, minBridgeProtocolVersion: configuredMinBridgeProtocolVersion, noConnectTimeoutMs, noInitResultTimeoutMs, } = options;
16
- // Callers can retire older SDKs, but cannot restore support for protocols the runtime no longer
17
- // implements. Omission uses the runtime minimum.
15
+ const { iframe, getInitialState, handlers, onManifest, onInitialized, onFailure, onLog, sandboxOrigin, minBridgeProtocolVersion: configuredMinBridgeProtocolVersion, noConnectTimeoutMs, initialStateTimeoutMs, noInitResultTimeoutMs, } = options;
16
+ // Callers can retire older SDKs, but cannot restore support for protocols
17
+ // the runtime no longer implements. Omission uses the runtime minimum.
18
18
  const minBridgeProtocolVersion = Math.max(configuredMinBridgeProtocolVersion ??
19
19
  MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION, MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION);
20
- let theme = initialState.theme;
21
- let contrastMode = initialState.contrastMode ?? DEFAULT_CONTRAST_MODE;
22
- let parent = initialState.parent;
23
- let page = initialState.page;
24
- let dataSources = initialState.dataSources;
20
+ let pendingState = {};
21
+ let hasPreparedInitialState = false;
22
+ let manifest;
25
23
  let activeDataSources;
26
24
  let sandboxBridgeProtocolVersion = LATEST_BRIDGE_PROTOCOL_VERSION;
27
- let manifest = initialState.manifest;
28
- let currentUser = initialState.currentUser;
29
- let pendingThemeChange = false;
30
- let pendingContrastModeChange = false;
31
- let pendingParentChange = false;
32
- let pendingPageChange = false;
33
- let pendingDataSourcesChange;
34
- let pendingCurrentUserChange = false;
35
25
  const querySubscriptions = new QuerySubscriptions();
36
26
  function emit(direction, payload) {
37
27
  onLog?.(direction, payload);
38
28
  }
39
- function post(message) {
40
- if (initializationController.status === "stopped") {
41
- // The parent page can stop this host. Do not send messages after the host stops.
42
- return;
29
+ function postToSandbox(message) {
30
+ const status = initializationController.status;
31
+ switch (status) {
32
+ case "waitingForConnect":
33
+ case "preparingInit":
34
+ case "waitingForInitResult":
35
+ case "initialized":
36
+ case "error":
37
+ break;
38
+ // A stopped host must not send messages, including pending responses.
39
+ // Return from the function to skip delivery below.
40
+ case "stopped":
41
+ return;
42
+ default:
43
+ return unreachable(status);
43
44
  }
44
45
  const sandboxMessage = normalizeHostToSandboxMessage(message, sandboxBridgeProtocolVersion);
45
46
  emit("out", sandboxMessage);
46
47
  iframe.contentWindow?.postMessage(sandboxMessage, sandboxOrigin);
47
48
  }
49
+ function sendInitMessage(message) {
50
+ postToSandbox(message);
51
+ }
52
+ function sendNack(message) {
53
+ postToSandbox(message);
54
+ }
55
+ function sendInitializedMessage(message) {
56
+ const status = initializationController.status;
57
+ switch (status) {
58
+ case "initialized":
59
+ postToSandbox(message);
60
+ return;
61
+ // Send state updates and handler results only after initialization succeeds.
62
+ case "waitingForConnect":
63
+ case "preparingInit":
64
+ case "waitingForInitResult":
65
+ case "error":
66
+ case "stopped":
67
+ return;
68
+ default:
69
+ unreachable(status);
70
+ }
71
+ }
48
72
  /**
49
73
  * Invokes a one-shot handler and posts its result with the request's ID.
50
74
  * Each result type is named `<request type>Result` by the bridge protocol.
@@ -59,7 +83,7 @@ export function createCustomBlockHost(options) {
59
83
  requestId: message.requestId,
60
84
  ...response,
61
85
  };
62
- post(result);
86
+ sendInitializedMessage(result);
63
87
  }
64
88
  function postQueryDataSourceResult(args) {
65
89
  const message = args.response.status === "error"
@@ -76,7 +100,7 @@ export function createCustomBlockHost(options) {
76
100
  items: args.response.items,
77
101
  hasMore: args.response.hasMore,
78
102
  };
79
- post(message);
103
+ sendInitializedMessage(message);
80
104
  }
81
105
  async function handleQuery(message, token = querySubscriptions.subscribe(message)) {
82
106
  const collectionSchema = getCollectionSchema(activeDataSources, message.dataSourceId);
@@ -99,88 +123,65 @@ export function createCustomBlockHost(options) {
99
123
  response,
100
124
  });
101
125
  }
102
- /**
103
- * Sends any pending host state changes to the sandbox. It is possible for these to change
104
- * between when the host sends `init` and when the sandbox responds with `initResult`. The host
105
- * does send any pending state changes until hearing back from the sandbox.
106
- */
126
+ function updateHostState(update) {
127
+ if (initializationController.status === "stopped" ||
128
+ initializationController.status === "error") {
129
+ return;
130
+ }
131
+ Object.assign(pendingState, update);
132
+ flushPendingHostStateChanges();
133
+ }
107
134
  function flushPendingHostStateChanges() {
108
- if (pendingThemeChange) {
109
- pendingThemeChange = false;
110
- post({ type: "themeChanged", theme });
135
+ // Keep updates queued until the sandbox acknowledges initialization.
136
+ // Updates received before init is sent are included in its payload instead.
137
+ if (initializationController.status !== "initialized") {
138
+ return;
139
+ }
140
+ const changes = pendingState;
141
+ pendingState = {};
142
+ if (changes.theme !== undefined) {
143
+ sendInitializedMessage({ type: "themeChanged", theme: changes.theme });
111
144
  }
112
- if (pendingContrastModeChange) {
113
- pendingContrastModeChange = false;
114
- post({ type: "contrastModeChanged", contrastMode });
145
+ if (changes.contrastMode !== undefined) {
146
+ sendInitializedMessage({
147
+ type: "contrastModeChanged",
148
+ contrastMode: changes.contrastMode,
149
+ });
115
150
  }
116
- if (pendingParentChange) {
117
- pendingParentChange = false;
118
- post({ type: "parentChanged", parent });
151
+ if (changes.parent !== undefined) {
152
+ sendInitializedMessage({ type: "parentChanged", parent: changes.parent });
119
153
  }
120
- if (pendingPageChange) {
121
- pendingPageChange = false;
122
- post({ type: "pageChanged", page });
154
+ if (changes.page !== undefined) {
155
+ sendInitializedMessage({ type: "pageChanged", page: changes.page });
123
156
  }
124
- if (pendingDataSourcesChange !== undefined) {
125
- const nextDataSources = pendingDataSourcesChange;
126
- pendingDataSourcesChange = undefined;
127
- post({
157
+ if (changes.dataSources !== undefined) {
158
+ sendInitializedMessage({
128
159
  type: "dataSourcesChanged",
129
- dataSources: nextDataSources,
160
+ dataSources: changes.dataSources,
130
161
  });
131
162
  }
132
- if (pendingCurrentUserChange) {
133
- pendingCurrentUserChange = false;
134
- post({ type: "currentUserChanged", currentUser });
163
+ if (changes.currentUser !== undefined) {
164
+ sendInitializedMessage({
165
+ type: "currentUserChanged",
166
+ currentUser: changes.currentUser,
167
+ });
135
168
  }
136
169
  }
137
170
  const initializationController = createInitializationController({
138
- blockId: initialState.blockId,
139
- getState: () => ({
140
- theme,
141
- contrastMode,
142
- parent,
143
- page,
144
- currentUser,
145
- }),
146
- prepareInit(connect) {
171
+ async prepareInit(connect) {
147
172
  sandboxBridgeProtocolVersion = connect.bridgeProtocolVersion;
148
- if (sandboxBridgeProtocolVersion <
149
- BRIDGE_PROTOCOL_VERSION_INTRODUCING_UNAVAILABLE_PAGE &&
150
- page.type === "unavailable") {
151
- return {
152
- status: "error",
153
- error: initErrorForFailureReason("context_unavailable"),
154
- };
155
- }
156
- manifest = connect.manifest ?? initialState.manifest;
157
- if (manifest === undefined) {
158
- return {
159
- status: "error",
160
- error: initErrorForFailureReason("manifest_unavailable"),
161
- };
162
- }
163
- onManifest?.(manifest);
164
- const resolvedDataSources = typeof dataSources === "function" ? dataSources(connect) : dataSources;
165
- const bindingErrorCode = getBindingErrorCode(manifest, resolvedDataSources);
166
- if (bindingErrorCode !== undefined) {
167
- return {
168
- status: "error",
169
- error: initErrorForFailureReason(bindingErrorCode),
170
- };
173
+ const result = await getInitialState(connect);
174
+ if (initializationController.status !== "preparingInit") {
175
+ return result;
171
176
  }
172
- activeDataSources = resolvedDataSources;
173
- return {
174
- status: "success",
175
- manifest,
176
- dataSources: resolvedDataSources,
177
- };
177
+ return prepareInitialState(result);
178
178
  },
179
- post,
179
+ sendInitMessage,
180
180
  warn: payload => emit("warn", payload),
181
181
  flushPendingHostStateChanges,
182
182
  minBridgeProtocolVersion,
183
183
  noConnectTimeoutMs,
184
+ initialStateTimeoutMs,
184
185
  noInitResultTimeoutMs,
185
186
  stopHostWithFailure: stopWithFailure,
186
187
  onInitResult(message) {
@@ -190,12 +191,39 @@ export function createCustomBlockHost(options) {
190
191
  },
191
192
  onFailure,
192
193
  });
194
+ function prepareInitialState(result) {
195
+ if (result.status === "error") {
196
+ return result;
197
+ }
198
+ manifest = result.initialState.manifest;
199
+ onManifest?.(manifest);
200
+ const initialState = { ...result.initialState, ...pendingState };
201
+ pendingState = {};
202
+ if (sandboxBridgeProtocolVersion <
203
+ BRIDGE_PROTOCOL_VERSION_INTRODUCING_UNAVAILABLE_PAGE &&
204
+ initialState.page.type === "unavailable") {
205
+ return {
206
+ status: "error",
207
+ error: initErrorForFailureReason("context_unavailable"),
208
+ };
209
+ }
210
+ const bindingErrorCode = getBindingErrorCode(initialState.manifest, initialState.dataSources);
211
+ if (bindingErrorCode !== undefined) {
212
+ return {
213
+ status: "error",
214
+ error: initErrorForFailureReason(bindingErrorCode),
215
+ };
216
+ }
217
+ activeDataSources = initialState.dataSources;
218
+ hasPreparedInitialState = true;
219
+ return { status: "success", initialState };
220
+ }
193
221
  const onMessage = createSandboxMessageRouter({
194
222
  iframe,
195
223
  sandboxOrigin,
196
224
  getInitStatus: () => initializationController.status,
197
225
  emit,
198
- post,
226
+ sendNack,
199
227
  onInvalidConnect: initializationController.handleInvalidConnect,
200
228
  onInvalidInitResult: initializationController.handleInvalidInitResult,
201
229
  onConnect: initializationController.handleConnect,
@@ -225,98 +253,46 @@ export function createCustomBlockHost(options) {
225
253
  }
226
254
  return {
227
255
  stop,
228
- post,
229
- setTheme(nextTheme) {
230
- theme = nextTheme;
231
- if (initializationController.status !== "success") {
232
- if (initializationController.status === "waitingForInitResult") {
233
- pendingThemeChange = true;
234
- }
235
- return;
236
- }
237
- const message = { type: "themeChanged", theme };
238
- post(message);
256
+ setTheme(theme) {
257
+ updateHostState({ theme });
239
258
  },
240
- setContrastMode(nextContrastMode) {
241
- contrastMode = nextContrastMode;
242
- if (initializationController.status !== "success") {
243
- if (initializationController.status === "waitingForInitResult") {
244
- pendingContrastModeChange = true;
245
- }
246
- return;
247
- }
248
- const message = {
249
- type: "contrastModeChanged",
250
- contrastMode,
251
- };
252
- post(message);
259
+ setContrastMode(contrastMode) {
260
+ updateHostState({ contrastMode });
253
261
  },
254
- setParent(nextParent) {
255
- parent = nextParent;
256
- if (initializationController.status !== "success") {
257
- if (initializationController.status === "waitingForInitResult") {
258
- pendingParentChange = true;
259
- }
260
- return;
261
- }
262
- const message = { type: "parentChanged", parent };
263
- post(message);
262
+ setParent(parent) {
263
+ updateHostState({ parent });
264
264
  },
265
- setPage(nextPage) {
266
- page = nextPage;
267
- if (nextPage.type === "unavailable" &&
265
+ setPage(page) {
266
+ if (page.type === "unavailable" &&
268
267
  sandboxBridgeProtocolVersion <
269
268
  BRIDGE_PROTOCOL_VERSION_INTRODUCING_UNAVAILABLE_PAGE &&
270
- (initializationController.status === "waitingForInitResult" ||
271
- initializationController.status === "success")) {
269
+ ((initializationController.status === "preparingInit" &&
270
+ hasPreparedInitialState) ||
271
+ initializationController.status === "waitingForInitResult" ||
272
+ initializationController.status === "initialized")) {
272
273
  stopWithFailure(initErrorForFailureReason("context_unavailable"));
273
274
  return;
274
275
  }
275
- if (initializationController.status !== "success") {
276
- if (initializationController.status === "waitingForInitResult") {
277
- pendingPageChange = true;
278
- }
279
- return;
280
- }
281
- const message = { type: "pageChanged", page };
282
- post(message);
276
+ updateHostState({ page });
277
+ },
278
+ setCurrentUser(currentUser) {
279
+ updateHostState({ currentUser });
283
280
  },
284
- setDataSources(nextDataSources) {
281
+ setDataSources(dataSources) {
285
282
  if (initializationController.hasConnectedSandbox &&
286
283
  manifest !== undefined &&
287
- getBindingErrorCode(manifest, nextDataSources) !== undefined) {
284
+ getBindingErrorCode(manifest, dataSources) !== undefined) {
288
285
  emit("warn", {
289
286
  message: "Ignored invalid data source bindings update",
290
- payload: nextDataSources,
287
+ payload: dataSources,
291
288
  });
292
289
  return;
293
290
  }
294
- dataSources = nextDataSources;
295
- activeDataSources = nextDataSources;
296
- if (initializationController.status !== "success") {
297
- if (initializationController.status === "waitingForInitResult") {
298
- pendingDataSourcesChange = nextDataSources;
299
- }
300
- return;
301
- }
302
- const message = {
303
- type: "dataSourcesChanged",
304
- dataSources: nextDataSources,
305
- };
306
- post(message);
307
- },
308
- setCurrentUser(nextCurrentUser) {
309
- currentUser = nextCurrentUser;
310
- if (initializationController.status !== "success") {
311
- if (initializationController.status === "waitingForInitResult") {
312
- pendingCurrentUserChange = true;
313
- }
314
- return;
315
- }
316
- post({ type: "currentUserChanged", currentUser });
291
+ activeDataSources = dataSources;
292
+ updateHostState({ dataSources });
317
293
  },
318
294
  refreshQuery({ dataSourceId }) {
319
- if (initializationController.status !== "success") {
295
+ if (initializationController.status !== "initialized") {
320
296
  return;
321
297
  }
322
298
  const refreshes = querySubscriptions.refresh(dataSourceId);
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export type { UpdatePageMessage } from "./protocol/messages/updatePage.js";
9
9
  export type { NotionPagePropertyValue, NotionPagePropertyWriteMap, NotionRichTextItem, } from "./protocol/pages/page.js";
10
10
  export { type AutoBindableProperty, getAutoBoundPropertyIdsByKey, } from "./autoBindProperties.js";
11
11
  export { type CustomBlockHostHandle, type CustomBlockHostOptions, createCustomBlockHost, } from "./createCustomBlockHost.js";
12
- export type { ConnectSuccessMessage, CustomBlockHostDataSourcesPayload, CustomBlockHostInitialDataSources, CustomBlockHostInitialState, } from "./lifecycle/types.js";
12
+ export type { ConnectSuccessMessage, CustomBlockHostDataSourcesPayload, CustomBlockHostGetInitialState, CustomBlockHostInitialState, CustomBlockHostInitialStateResult, } from "./lifecycle/types.js";
13
13
  export type { CreatePageMessageInput, CustomBlockHostCreatePageResult, CustomBlockHostFailure, CustomBlockHostGetPageResult, CustomBlockHostGetUserResult, CustomBlockHostHandlers, CustomBlockHostListUsersResult, CustomBlockHostLogDirection, CustomBlockHostOpenPageResult, CustomBlockHostUpdatePageResult, GetPageMessageInput, GetUserMessageInput, ListUsersMessageInput, OpenPageMessageInput, QueryDataSourceMessageInput, ResizeMessageInput, UpdatePageMessageInput, } from "./messages/types.js";
14
14
  export type { CustomBlockHostQueryDataSourceResult } from "./queries/types.js";
15
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,iBAAiB,EAAE,MAAM,yDAAyD,CAAA;AAChG,YAAY,EAAE,cAAc,EAAE,MAAM,sDAAsD,CAAA;AAC1F,YAAY,EAAE,cAAc,EAAE,MAAM,sDAAsD,CAAA;AAC1F,YAAY,EAAE,gBAAgB,EAAE,MAAM,wDAAwD,CAAA;AAC9F,YAAY,EAAE,eAAe,EAAE,MAAM,uDAAuD,CAAA;AAC5F,YAAY,EAAE,sBAAsB,EAAE,MAAM,8DAA8D,CAAA;AAC1G,YAAY,EAAE,aAAa,EAAE,MAAM,qDAAqD,CAAA;AACxF,YAAY,EAAE,iBAAiB,EAAE,MAAM,yDAAyD,CAAA;AAChG,YAAY,EACX,uBAAuB,EACvB,0BAA0B,EAC1B,kBAAkB,GAClB,MAAM,gDAAgD,CAAA;AACvD,OAAO,EACN,KAAK,oBAAoB,EACzB,4BAA4B,GAC5B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACN,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,qBAAqB,GACrB,MAAM,4BAA4B,CAAA;AACnC,YAAY,EACX,qBAAqB,EACrB,iCAAiC,EACjC,iCAAiC,EACjC,2BAA2B,GAC3B,MAAM,sBAAsB,CAAA;AAC7B,YAAY,EACX,sBAAsB,EACtB,+BAA+B,EAC/B,sBAAsB,EACtB,4BAA4B,EAC5B,4BAA4B,EAC5B,uBAAuB,EACvB,8BAA8B,EAC9B,2BAA2B,EAC3B,6BAA6B,EAC7B,+BAA+B,EAC/B,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,EACpB,2BAA2B,EAC3B,kBAAkB,EAClB,sBAAsB,GACtB,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,oCAAoC,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,iBAAiB,EAAE,MAAM,yDAAyD,CAAA;AAChG,YAAY,EAAE,cAAc,EAAE,MAAM,sDAAsD,CAAA;AAC1F,YAAY,EAAE,cAAc,EAAE,MAAM,sDAAsD,CAAA;AAC1F,YAAY,EAAE,gBAAgB,EAAE,MAAM,wDAAwD,CAAA;AAC9F,YAAY,EAAE,eAAe,EAAE,MAAM,uDAAuD,CAAA;AAC5F,YAAY,EAAE,sBAAsB,EAAE,MAAM,8DAA8D,CAAA;AAC1G,YAAY,EAAE,aAAa,EAAE,MAAM,qDAAqD,CAAA;AACxF,YAAY,EAAE,iBAAiB,EAAE,MAAM,yDAAyD,CAAA;AAChG,YAAY,EACX,uBAAuB,EACvB,0BAA0B,EAC1B,kBAAkB,GAClB,MAAM,gDAAgD,CAAA;AACvD,OAAO,EACN,KAAK,oBAAoB,EACzB,4BAA4B,GAC5B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACN,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,qBAAqB,GACrB,MAAM,4BAA4B,CAAA;AACnC,YAAY,EACX,qBAAqB,EACrB,iCAAiC,EACjC,8BAA8B,EAC9B,2BAA2B,EAC3B,iCAAiC,GACjC,MAAM,sBAAsB,CAAA;AAC7B,YAAY,EACX,sBAAsB,EACtB,+BAA+B,EAC/B,sBAAsB,EACtB,4BAA4B,EAC5B,4BAA4B,EAC5B,uBAAuB,EACvB,8BAA8B,EAC9B,2BAA2B,EAC3B,6BAA6B,EAC7B,+BAA+B,EAC/B,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,EACpB,2BAA2B,EAC3B,kBAAkB,EAClB,sBAAsB,GACtB,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,oCAAoC,EAAE,MAAM,oBAAoB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"initErrors.d.ts","sourceRoot":"","sources":["../../src/lifecycle/initErrors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,wBAAwB,EACxB,wBAAwB,EACxB,MAAM,mDAAmD,CAAA;AAK1D;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACxC,MAAM,EAAE,wBAAwB,EAChC,IAAI,GAAE;IACL,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,4BAA4B,CAAC,EAAE,MAAM,CAAA;CAChC,GACJ,wBAAwB,CA+I1B"}
1
+ {"version":3,"file":"initErrors.d.ts","sourceRoot":"","sources":["../../src/lifecycle/initErrors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,wBAAwB,EACxB,wBAAwB,EACxB,MAAM,mDAAmD,CAAA;AAK1D;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACxC,MAAM,EAAE,wBAAwB,EAChC,IAAI,GAAE;IACL,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,4BAA4B,CAAC,EAAE,MAAM,CAAA;CAChC,GACJ,wBAAwB,CAqJ1B"}
@@ -77,6 +77,12 @@ export function initErrorForFailureReason(reason, data = {}) {
77
77
  message: "Current user is unavailable.",
78
78
  isRetryable: true,
79
79
  };
80
+ case "initial_state_timeout":
81
+ return {
82
+ code: reason,
83
+ message: "Host did not prepare initial state before the timeout.",
84
+ isRetryable: true,
85
+ };
80
86
  case "missing_data_source_binding":
81
87
  return {
82
88
  code: reason,
@@ -1,40 +1,16 @@
1
- import type { NotionContrastMode } from "../protocol/contrast.js";
2
- import type { NotionBlockId } from "../protocol/ids.js";
3
- import type { CustomBlockManifest } from "../protocol/manifest.js";
4
1
  import type { ConnectMessage } from "../protocol/messages/connect.js";
5
- import type { HostToSandboxMessage } from "../protocol/messages/hostToSandbox.js";
6
- import type { CustomBlockInitErrorInfo } from "../protocol/messages/init.js";
2
+ import type { CustomBlockInitErrorInfo, InitMessage } from "../protocol/messages/init.js";
7
3
  import type { InitResultMessage } from "../protocol/messages/initResult.js";
8
- import type { CustomBlockPage } from "../protocol/pages/page.js";
9
- import type { NotionParent } from "../protocol/parent.js";
10
- import type { NotionTheme } from "../protocol/theme.js";
11
- import type { NotionUser } from "../protocol/users/user.js";
12
4
  import type { CustomBlockHostFailure } from "../messages/types.js";
13
- import type { ConnectSuccessMessage, CustomBlockHostDataSourcesPayload, InitStatus } from "./types.js";
14
- type InitializationState = {
15
- theme: NotionTheme;
16
- contrastMode: NotionContrastMode;
17
- parent: NotionParent;
18
- page: CustomBlockPage;
19
- currentUser: NotionUser;
20
- };
21
- type InitializationPreparation = {
22
- status: "success";
23
- manifest: CustomBlockManifest;
24
- dataSources: CustomBlockHostDataSourcesPayload;
25
- } | {
26
- status: "error";
27
- error: CustomBlockInitErrorInfo;
28
- };
5
+ import type { ConnectSuccessMessage, CustomBlockHostInitialStateResult, InitStatus } from "./types.js";
29
6
  export type InitializationControllerOptions = {
30
- blockId: NotionBlockId;
31
- getState: () => InitializationState;
32
- prepareInit: (connect: ConnectSuccessMessage) => InitializationPreparation;
33
- post: (message: HostToSandboxMessage) => void;
7
+ prepareInit: (connect: ConnectSuccessMessage) => Promise<CustomBlockHostInitialStateResult>;
8
+ sendInitMessage: (message: InitMessage) => void;
34
9
  warn: (payload: unknown) => void;
35
10
  flushPendingHostStateChanges: () => void;
36
11
  minBridgeProtocolVersion: number;
37
12
  noConnectTimeoutMs?: number;
13
+ initialStateTimeoutMs?: number;
38
14
  noInitResultTimeoutMs?: number;
39
15
  stopHostWithFailure: (error: CustomBlockHostFailure) => void;
40
16
  onInitResult?: (message: InitResultMessage) => void;
@@ -58,5 +34,4 @@ export type InitializationController = {
58
34
  stop: () => void;
59
35
  };
60
36
  export declare function createInitializationController(options: InitializationControllerOptions): InitializationController;
61
- export {};
62
37
  //# sourceMappingURL=initializationController.d.ts.map