@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.
- package/HOST.md +68 -37
- package/bin/src/bridge/manifest.js +3 -3
- package/dist/bridge/SandboxBridge.d.ts +6 -12
- package/dist/bridge/SandboxBridge.d.ts.map +1 -1
- package/dist/bridge/SandboxBridge.js +123 -71
- package/dist/bridge/dataSources/resolve.d.ts +3 -4
- package/dist/bridge/dataSources/resolve.d.ts.map +1 -1
- package/dist/bridge/dataSources/resolve.js +2 -12
- package/dist/bridge/hostState.d.ts +3 -2
- package/dist/bridge/hostState.d.ts.map +1 -1
- package/dist/bridge/loadManifest.d.ts +4 -4
- package/dist/bridge/loadManifest.d.ts.map +1 -1
- package/dist/bridge/loadManifest.js +2 -2
- package/dist/bridge/manifest.d.ts +3 -3
- package/dist/bridge/manifest.js +3 -3
- package/dist/bridge/messages/{ready.d.ts → connect.d.ts} +13 -11
- package/dist/bridge/messages/connect.d.ts.map +1 -0
- package/dist/bridge/messages/{ready.js → connect.js} +14 -13
- package/dist/bridge/messages/hostToSandbox.d.ts +24 -79
- package/dist/bridge/messages/hostToSandbox.d.ts.map +1 -1
- package/dist/bridge/messages/hostToSandbox.js +2 -3
- package/dist/bridge/messages/init.d.ts +35 -6
- package/dist/bridge/messages/init.d.ts.map +1 -1
- package/dist/bridge/messages/init.js +8 -4
- package/dist/bridge/messages/initResult.d.ts +29 -0
- package/dist/bridge/messages/initResult.d.ts.map +1 -0
- package/dist/bridge/messages/initResult.js +25 -0
- package/dist/bridge/messages/queryDataSource.d.ts +3 -7
- package/dist/bridge/messages/queryDataSource.d.ts.map +1 -1
- package/dist/bridge/messages/queryDataSource.js +3 -7
- package/dist/bridge/messages/queryDataSourceResult.d.ts +4 -240
- package/dist/bridge/messages/queryDataSourceResult.d.ts.map +1 -1
- package/dist/bridge/messages/queryDataSourceResult.js +4 -22
- package/dist/bridge/messages/sandboxToHost.d.ts +21 -7
- package/dist/bridge/messages/sandboxToHost.d.ts.map +1 -1
- package/dist/bridge/messages/sandboxToHost.js +4 -2
- package/dist/bridge/sandboxClient.d.ts +2 -4
- package/dist/bridge/sandboxClient.d.ts.map +1 -1
- package/dist/bridge/sandboxClient.js +2 -5
- package/dist/customBlock.d.ts +1 -1
- package/dist/customBlock.d.ts.map +1 -1
- package/dist/customBlock.js +1 -1
- package/dist/host/createCustomBlockHost.d.ts +6 -3
- package/dist/host/createCustomBlockHost.d.ts.map +1 -1
- package/dist/host/createCustomBlockHost.js +227 -58
- package/dist/host/lifecycle/initErrors.d.ts +1 -1
- package/dist/host/lifecycle/initErrors.d.ts.map +1 -1
- package/dist/host/lifecycle/initErrors.js +25 -7
- package/dist/host/lifecycle/types.d.ts +6 -4
- package/dist/host/lifecycle/types.d.ts.map +1 -1
- package/dist/host/messages/invalidSandboxMessage.js +0 -3
- package/dist/host/queries/querySubscriptions.d.ts +10 -10
- package/dist/host/queries/querySubscriptions.d.ts.map +1 -1
- package/dist/host/queries/querySubscriptions.js +35 -19
- package/dist/host/queries/types.d.ts +5 -4
- package/dist/host/queries/types.d.ts.map +1 -1
- package/dist/host.d.ts +5 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/init.d.ts +7 -7
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +70 -50
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/standalonePreview.d.ts.map +1 -1
- package/dist/react/standalonePreview.js +2 -0
- package/dist/react/useCustomBlockInit.d.ts +5 -6
- package/dist/react/useCustomBlockInit.d.ts.map +1 -1
- package/dist/react/useCustomBlockInit.js +3 -19
- package/dist/react/useRuntimeState.d.ts +4 -3
- package/dist/react/useRuntimeState.d.ts.map +1 -1
- package/dist/react/useRuntimeState.js +3 -2
- package/dist/version.js +1 -1
- package/docs/data-sources.md +7 -5
- package/docs/errors.md +5 -1
- package/docs/lifecycle.md +19 -31
- package/docs/manifest.md +1 -1
- package/package.json +1 -1
- package/src/bridge/SandboxBridge.ts +151 -77
- package/src/bridge/dataSources/resolve.ts +3 -14
- package/src/bridge/hostState.ts +3 -2
- package/src/bridge/loadManifest.ts +6 -6
- package/src/bridge/manifest.ts +3 -3
- package/src/bridge/messages/{ready.ts → connect.ts} +15 -14
- package/src/bridge/messages/hostToSandbox.ts +2 -6
- package/src/bridge/messages/init.ts +24 -10
- package/src/bridge/messages/initResult.ts +37 -0
- package/src/bridge/messages/queryDataSource.ts +3 -7
- package/src/bridge/messages/queryDataSourceResult.ts +4 -24
- package/src/bridge/messages/sandboxToHost.ts +4 -2
- package/src/bridge/sandboxClient.ts +3 -8
- package/src/customBlock.ts +2 -2
- package/src/host/createCustomBlockHost.ts +269 -64
- package/src/host/lifecycle/initErrors.ts +25 -7
- package/src/host/lifecycle/types.ts +13 -4
- package/src/host/messages/invalidSandboxMessage.ts +0 -3
- package/src/host/queries/querySubscriptions.ts +58 -39
- package/src/host/queries/types.ts +10 -11
- package/src/host.ts +13 -2
- package/src/index.ts +3 -1
- package/src/init.ts +73 -60
- package/src/react/index.ts +0 -1
- package/src/react/standalonePreview.ts +2 -0
- package/src/react/useCustomBlockInit.ts +6 -27
- package/src/react/useRuntimeState.ts +4 -3
- package/dist/bridge/messages/ready.d.ts.map +0 -1
package/docs/lifecycle.md
CHANGED
|
@@ -4,9 +4,15 @@ The SDK ↔ host handshake, the React wrapper that runs it, and the auto-resize
|
|
|
4
4
|
|
|
5
5
|
## Handshake
|
|
6
6
|
|
|
7
|
-
`initCustomBlock()`
|
|
7
|
+
`initCustomBlock()` completes a three-message handshake:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
1. The SDK sends `connect` with a new initialization ID and some version info.
|
|
10
|
+
2. The host replies with `init`, echoing that initialization ID and providing the manifest, block context, current user, and data source bindings.
|
|
11
|
+
3. The SDK confirms that state is compatible and sends `initResult` with the same initialization ID.
|
|
12
|
+
|
|
13
|
+
The promise resolves only after the SDK sends `initResult.success`. Await it before mounting React so hooks always see initialized state.
|
|
14
|
+
|
|
15
|
+
- Rejects with `CustomBlockInitializationError` code `init_timeout` if the host doesn't respond.
|
|
10
16
|
- In a top-level browser tab (no parent frame), rejects with `NotInIframeError` code `not_in_iframe`. `<NotionCustomBlock>` catches this, seeds placeholders, and renders `children` behind a warning banner so dev-time previews still work.
|
|
11
17
|
- After init, `*Changed` events (e.g. `themeChanged`, `parentChanged`, `dataSourcesChanged`) push updates and the relevant hooks re-render.
|
|
12
18
|
- `initCustomBlock` is idempotent; subsequent calls return the same promise.
|
|
@@ -44,7 +50,7 @@ function useCustomBlockInit(
|
|
|
44
50
|
|
|
45
51
|
type UseCustomBlockInitResult =
|
|
46
52
|
| { isLoaded: false; error: undefined }
|
|
47
|
-
| { isLoaded: false; error:
|
|
53
|
+
| { isLoaded: false; error: CustomBlockInitializationError }
|
|
48
54
|
| { isLoaded: true; error: undefined; initial: CustomBlockInitPayload };
|
|
49
55
|
```
|
|
50
56
|
|
|
@@ -69,39 +75,21 @@ function initCustomBlock(
|
|
|
69
75
|
type InitCustomBlockOptions = { timeoutMs?: number };
|
|
70
76
|
```
|
|
71
77
|
|
|
72
|
-
The lower-level promise API. `<NotionCustomBlock>` and `useCustomBlockInit` both call it for you. Reach for it directly only when you want to `await` init at module scope (e.g. before `ReactDOM.createRoot`).
|
|
78
|
+
The lower-level promise API. `<NotionCustomBlock>` and `useCustomBlockInit` both call it for you. Reach for it directly only when you want to `await` init at module scope (e.g. before `ReactDOM.createRoot`). Expected failures reject with `CustomBlockInitializationError`.
|
|
73
79
|
|
|
74
|
-
### `
|
|
80
|
+
### `CustomBlockInitializationError`
|
|
75
81
|
|
|
76
|
-
|
|
77
|
-
class CustomBlockInitError extends Error {
|
|
78
|
-
code: CustomBlockInitErrorCode;
|
|
79
|
-
isRetryable: boolean;
|
|
80
|
-
}
|
|
82
|
+
The code tells you where initialization failed:
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
| "manifest_unavailable"
|
|
86
|
-
| "manifest_invalid"
|
|
87
|
-
| "invalid_protocol_version"
|
|
88
|
-
| "unsupported_protocol_version"
|
|
89
|
-
| "context_unavailable"
|
|
90
|
-
| "current_user_unavailable"
|
|
91
|
-
| "missing_data_source_binding"
|
|
92
|
-
| "data_source_unavailable"
|
|
93
|
-
| "missing_property_binding"
|
|
94
|
-
| "invalid_property_binding"
|
|
95
|
-
| "not_in_iframe"
|
|
96
|
-
| "init_timeout"
|
|
97
|
-
| "unknown_error";
|
|
98
|
-
```
|
|
84
|
+
- `CustomBlockInitErrorCode`: the host rejected initialization in `init.error`.
|
|
85
|
+
- `CustomBlockInitResultErrorCode`: the SDK could not apply the host's state and reported why in `initResult.error`.
|
|
86
|
+
- `not_in_iframe` and `init_timeout`: the SDK failed before the handshake completed.
|
|
99
87
|
|
|
100
|
-
|
|
88
|
+
All code types are open string unions, so keep a default branch. Use `isRetryable` to decide whether offering a retry makes sense, and treat `message` as display text rather than a stable identifier. See [Error handling](./errors.md) for the shared error contract.
|
|
101
89
|
|
|
102
90
|
### `customBlock`
|
|
103
91
|
|
|
104
|
-
Framework-neutral runtime APIs for renderers that do not use React hooks. `customBlock.getState()` returns a `CustomBlockState` snapshot that hides internal query cache details. Initialized-only getters (`getTheme`, `getBlockId`, `getParent
|
|
92
|
+
Framework-neutral runtime APIs for renderers that do not use React hooks. `customBlock.getState()` returns a `CustomBlockState` snapshot that hides internal query cache details. Initialized-only getters (e.g. `getTheme`, `getBlockId`, `getParent`) throw until `initCustomBlock()` resolves.
|
|
105
93
|
|
|
106
94
|
`customBlock` covers runtime state and sizing. Row querying still goes through `useDataSource`, while imperative APIs such as `pages.*` and `users.*` are already framework-neutral functions.
|
|
107
95
|
|
|
@@ -127,7 +115,7 @@ stopAutoResize();
|
|
|
127
115
|
|
|
128
116
|
### `NotInIframeError`
|
|
129
117
|
|
|
130
|
-
Thrown when `initCustomBlock` is called in a top-level tab (no parent frame). It has `code: "not_in_iframe"` and `isRetryable: false`. `<NotionCustomBlock>` catches it and falls back to a standalone preview with a warning banner
|
|
118
|
+
Thrown when `initCustomBlock` is called in a top-level tab (no parent frame). It extends `CustomBlockInitializationError`, has `code: "not_in_iframe"` and `isRetryable: false`, and can be detected specifically with `instanceof NotInIframeError`. `<NotionCustomBlock>` catches it and falls back to a standalone preview with a warning banner.
|
|
131
119
|
|
|
132
120
|
### `useCustomBlockAutoResize({ enabled? })`
|
|
133
121
|
|
|
@@ -154,7 +142,7 @@ function App() {
|
|
|
154
142
|
Press `\` while focused in a custom block to toggle a debug overlay that replaces the block's children with a `<pre>` log of every `postMessage` sent and received over the bridge. Each line is formatted as:
|
|
155
143
|
|
|
156
144
|
```
|
|
157
|
-
[ISO timestamp] sent/received: {"type":"
|
|
145
|
+
[ISO timestamp] sent/received: {"type":"connect", …}
|
|
158
146
|
```
|
|
159
147
|
|
|
160
148
|
The log is intentionally plain — no filtering or decoration — so it can be copied and pasted directly to a local coding agent for debugging.
|
package/docs/manifest.md
CHANGED
|
@@ -18,7 +18,7 @@ A custom block declares its required data sources in `custom_blocks.json` at the
|
|
|
18
18
|
}
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
`initCustomBlock()` fetches the manifest and forwards it with `
|
|
21
|
+
`initCustomBlock()` fetches the manifest and forwards it with `connect`. The `notionCustomBlock()` Vite plugin from `@notionhq/custom-blocks/vite` serves it in dev and emits it into `dist/` on build. If the file is missing, the SDK omits `manifest` from `connect`. If the file is unavailable for another reason or invalid, the SDK sends `connect` with `status: "error"` and an `error` payload. The host returns its authoritative manifest in `init`. The SDK uses that manifest even when it differs from the manifest sent in `connect`.
|
|
22
22
|
|
|
23
23
|
## Vite plugin
|
|
24
24
|
|
package/package.json
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { readIncomingType } from "./incomingType.js"
|
|
33
33
|
import type { ManifestLoadResult } from "./loadManifest.js"
|
|
34
34
|
import type { CustomBlockManifest } from "./manifest.js"
|
|
35
|
+
import type { ConnectMessage } from "./messages/connect.js"
|
|
35
36
|
import type {
|
|
36
37
|
CreatePageMessage,
|
|
37
38
|
CreatePageMessageParent,
|
|
@@ -39,12 +40,19 @@ import type {
|
|
|
39
40
|
import type { CustomBlockCreatePageErrorInfo } from "./messages/createPageResult.js"
|
|
40
41
|
import type { GetUserMessage } from "./messages/getUser.js"
|
|
41
42
|
import { hostToSandboxMessageSchema } from "./messages/hostToSandbox.js"
|
|
42
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
CustomBlockInitializationError,
|
|
45
|
+
type InitMessage,
|
|
46
|
+
initMessageSchema,
|
|
47
|
+
} from "./messages/init.js"
|
|
48
|
+
import type {
|
|
49
|
+
CustomBlockInitResultErrorInfo,
|
|
50
|
+
InitResultMessage,
|
|
51
|
+
} from "./messages/initResult.js"
|
|
43
52
|
import type { InvalidHostMessage } from "./messages/invalidHostMessage.js"
|
|
44
53
|
import type { ListUsersMessage } from "./messages/listUsers.js"
|
|
45
54
|
import type { QueryDataSourceMessage } from "./messages/queryDataSource.js"
|
|
46
55
|
import type { CustomBlockQueryDataSourceErrorInfo } from "./messages/queryDataSourceResult.js"
|
|
47
|
-
import type { ReadyMessage } from "./messages/ready.js"
|
|
48
56
|
import type { ResizeMessage } from "./messages/resize.js"
|
|
49
57
|
import type { UpdatePageMessage } from "./messages/updatePage.js"
|
|
50
58
|
import { PendingRequests } from "./pendingRequests.js"
|
|
@@ -55,7 +63,6 @@ import type { NotionUser } from "./users/user.js"
|
|
|
55
63
|
* single host needs to support multiple custom blocks built with different versions of the bridge
|
|
56
64
|
* protocol. Increment this number any time a breaking change is made to the bridge protocol.
|
|
57
65
|
*/
|
|
58
|
-
// TODO(custom-blocks): Update when bumping bridge protocol version to 3.
|
|
59
66
|
export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 2
|
|
60
67
|
|
|
61
68
|
/**
|
|
@@ -92,20 +99,22 @@ export class SandboxBridge {
|
|
|
92
99
|
private readonly pendingUpdatePage = new PendingRequests<UpdatePageResult>(
|
|
93
100
|
"custom-block-update-page",
|
|
94
101
|
)
|
|
95
|
-
private
|
|
102
|
+
private hasSentConnect = false
|
|
103
|
+
private hasReceivedInit = false
|
|
104
|
+
private initializationId: string | undefined
|
|
96
105
|
private latestDataSourceBindings: NotionDataSourceBindings = {}
|
|
97
|
-
private
|
|
106
|
+
private isMockState = false
|
|
107
|
+
private resolveInit: (() => void) | undefined
|
|
98
108
|
private rejectInit: ((reason: Error) => void) | undefined
|
|
99
|
-
private readonly initMessage: Promise<
|
|
109
|
+
private readonly initMessage: Promise<void> = new Promise(
|
|
100
110
|
(resolve, reject) => {
|
|
101
111
|
this.resolveInit = resolve
|
|
102
112
|
this.rejectInit = reject
|
|
103
113
|
},
|
|
104
114
|
)
|
|
105
|
-
private manifest: CustomBlockManifest | null = null
|
|
106
115
|
|
|
107
116
|
constructor() {
|
|
108
|
-
// `
|
|
117
|
+
// `connect` is sent later by `initCustomBlock` (after the manifest fetch
|
|
109
118
|
// resolves). Top-level / no-iframe rejection is handled there too, so
|
|
110
119
|
// the constructor just attaches the listener.
|
|
111
120
|
if (typeof window !== "undefined") {
|
|
@@ -138,7 +147,7 @@ export class SandboxBridge {
|
|
|
138
147
|
return () => this.messageLogListeners.delete(listener)
|
|
139
148
|
}
|
|
140
149
|
|
|
141
|
-
awaitInit(signal?: AbortSignal): Promise<
|
|
150
|
+
awaitInit(signal?: AbortSignal): Promise<void> {
|
|
142
151
|
if (!signal) {
|
|
143
152
|
return this.initMessage
|
|
144
153
|
}
|
|
@@ -150,9 +159,9 @@ export class SandboxBridge {
|
|
|
150
159
|
const onAbort = () => reject(signal.reason)
|
|
151
160
|
signal.addEventListener("abort", onAbort, { once: true })
|
|
152
161
|
this.initMessage.then(
|
|
153
|
-
|
|
162
|
+
() => {
|
|
154
163
|
signal.removeEventListener("abort", onAbort)
|
|
155
|
-
resolve(
|
|
164
|
+
resolve()
|
|
156
165
|
},
|
|
157
166
|
err => {
|
|
158
167
|
signal.removeEventListener("abort", onAbort)
|
|
@@ -162,34 +171,38 @@ export class SandboxBridge {
|
|
|
162
171
|
})
|
|
163
172
|
}
|
|
164
173
|
|
|
165
|
-
|
|
174
|
+
sendConnect(manifestResult: ManifestLoadResult) {
|
|
166
175
|
if (typeof window === "undefined") {
|
|
167
176
|
return
|
|
168
177
|
}
|
|
169
|
-
if (this.
|
|
170
|
-
console.warn("[custom-blocks-sdk] ignoring duplicate
|
|
178
|
+
if (this.hasSentConnect) {
|
|
179
|
+
console.warn("[custom-blocks-sdk] ignoring duplicate connect message")
|
|
171
180
|
return
|
|
172
181
|
}
|
|
173
182
|
const { manifest, error } = manifestResult
|
|
174
|
-
this.
|
|
175
|
-
|
|
176
|
-
|
|
183
|
+
this.hasSentConnect = true
|
|
184
|
+
const initializationId = `custom-block-initialization-${this.nextRequestId}`
|
|
185
|
+
this.nextRequestId += 1
|
|
186
|
+
this.initializationId = initializationId
|
|
187
|
+
const connectMessage: ConnectMessage =
|
|
177
188
|
error !== undefined
|
|
178
189
|
? {
|
|
179
|
-
type: "
|
|
190
|
+
type: "connect",
|
|
191
|
+
initializationId,
|
|
180
192
|
status: "error",
|
|
181
193
|
bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
|
|
182
194
|
sdkVersion: CUSTOM_BLOCKS_SDK_VERSION,
|
|
183
195
|
error,
|
|
184
196
|
}
|
|
185
197
|
: {
|
|
186
|
-
type: "
|
|
198
|
+
type: "connect",
|
|
199
|
+
initializationId,
|
|
187
200
|
status: "success",
|
|
188
201
|
bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
|
|
189
202
|
sdkVersion: CUSTOM_BLOCKS_SDK_VERSION,
|
|
190
|
-
manifest,
|
|
203
|
+
...(manifest !== null ? { manifest } : {}),
|
|
191
204
|
}
|
|
192
|
-
this.postToHost(
|
|
205
|
+
this.postToHost(connectMessage)
|
|
193
206
|
}
|
|
194
207
|
|
|
195
208
|
private postToHost(message: unknown) {
|
|
@@ -254,7 +267,18 @@ export class SandboxBridge {
|
|
|
254
267
|
// `init` is the only message valid before initialization. Handle it up
|
|
255
268
|
// front so every later case can assume `status === "initialized"`.
|
|
256
269
|
if (message.type === "init") {
|
|
257
|
-
this.
|
|
270
|
+
if (message.initializationId !== this.initializationId) {
|
|
271
|
+
console.warn(
|
|
272
|
+
`[custom-blocks-sdk] ignoring init for unknown initializationId ${message.initializationId}`,
|
|
273
|
+
)
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
if (this.hasReceivedInit) {
|
|
277
|
+
console.warn("[custom-blocks-sdk] ignoring duplicate init message")
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
this.hasReceivedInit = true
|
|
281
|
+
this.applyInit(message, true)
|
|
258
282
|
return
|
|
259
283
|
}
|
|
260
284
|
|
|
@@ -309,10 +333,12 @@ export class SandboxBridge {
|
|
|
309
333
|
const dataSources = reuseDataSourcesForUnchangedBindings({
|
|
310
334
|
previousDataSources: hostState.dataSources,
|
|
311
335
|
previousBindings: this.latestDataSourceBindings,
|
|
312
|
-
nextDataSources:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
336
|
+
nextDataSources: this.isMockState
|
|
337
|
+
? resolveMockDataSources(nextBindings)
|
|
338
|
+
: resolveDataSources({
|
|
339
|
+
manifest: hostState.manifest,
|
|
340
|
+
dataSourceBindings: nextBindings,
|
|
341
|
+
}),
|
|
316
342
|
nextBindings,
|
|
317
343
|
})
|
|
318
344
|
this.latestDataSourceBindings = nextBindings
|
|
@@ -400,32 +426,23 @@ export class SandboxBridge {
|
|
|
400
426
|
|
|
401
427
|
case "queryDataSourceResult": {
|
|
402
428
|
const queryEntry = Object.entries(hostState.dataSourceState).find(
|
|
403
|
-
([, state]) => state.
|
|
429
|
+
([, state]) => state.subscriptionId === message.subscriptionId,
|
|
404
430
|
)
|
|
405
431
|
if (queryEntry === undefined) {
|
|
406
432
|
return
|
|
407
433
|
}
|
|
408
434
|
const [key, currentState] = queryEntry
|
|
409
|
-
if (currentState.latestRequestId !== message.requestId) {
|
|
410
|
-
return
|
|
411
|
-
}
|
|
412
435
|
const queryResult =
|
|
413
|
-
|
|
414
|
-
?
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
: {
|
|
421
|
-
items: message.items,
|
|
422
|
-
hasMore: message.hasMore,
|
|
423
|
-
error: undefined,
|
|
424
|
-
}
|
|
436
|
+
message.status === "error"
|
|
437
|
+
? {
|
|
438
|
+
items: [],
|
|
439
|
+
hasMore: false,
|
|
440
|
+
error: message.error,
|
|
441
|
+
}
|
|
425
442
|
: {
|
|
426
443
|
items: message.items,
|
|
427
444
|
hasMore: message.hasMore,
|
|
428
|
-
error:
|
|
445
|
+
error: undefined,
|
|
429
446
|
}
|
|
430
447
|
this.hostState = {
|
|
431
448
|
...hostState,
|
|
@@ -436,10 +453,7 @@ export class SandboxBridge {
|
|
|
436
453
|
isLoading: false,
|
|
437
454
|
hasMore: queryResult.hasMore,
|
|
438
455
|
error: queryResult.error,
|
|
439
|
-
|
|
440
|
-
// same subscription still match.
|
|
441
|
-
latestRequestId: message.requestId,
|
|
442
|
-
latestSnapshotId: message.snapshotId,
|
|
456
|
+
subscriptionId: message.subscriptionId,
|
|
443
457
|
latestLimit: currentState.latestLimit,
|
|
444
458
|
},
|
|
445
459
|
},
|
|
@@ -463,16 +477,6 @@ export class SandboxBridge {
|
|
|
463
477
|
return this.hostState
|
|
464
478
|
}
|
|
465
479
|
|
|
466
|
-
/**
|
|
467
|
-
* The author-declared manifest loaded from `custom_blocks.json` and forwarded
|
|
468
|
-
* to the host in `ready`. `null` when it failed to load/parse and the host
|
|
469
|
-
* should reject init via `ready.status: "error"`. Static for the lifetime
|
|
470
|
-
* of the sandbox.
|
|
471
|
-
*/
|
|
472
|
-
getManifest(): CustomBlockManifest | null {
|
|
473
|
-
return this.manifest
|
|
474
|
-
}
|
|
475
|
-
|
|
476
480
|
/**
|
|
477
481
|
* Apply an `init` payload as if it had arrived from the host. Lets callers
|
|
478
482
|
* seed the bridge directly (e.g. the React provider's standalone preview
|
|
@@ -480,10 +484,14 @@ export class SandboxBridge {
|
|
|
480
484
|
* of why it's being seeded.
|
|
481
485
|
*/
|
|
482
486
|
setMockState(message: InitMessage) {
|
|
483
|
-
this.
|
|
487
|
+
this.isMockState = true
|
|
488
|
+
this.applyInit(v.parse(initMessageSchema, message), false)
|
|
484
489
|
}
|
|
485
490
|
|
|
486
|
-
private applyInit(message: InitMessage) {
|
|
491
|
+
private applyInit(message: InitMessage, postResult: boolean) {
|
|
492
|
+
if (postResult) {
|
|
493
|
+
this.isMockState = false
|
|
494
|
+
}
|
|
487
495
|
if (message.status === "error") {
|
|
488
496
|
// The host couldn't construct block location for this block (most commonly the parent record
|
|
489
497
|
// failed to resolve). Surface the failure through the `awaitInit` promise so callers see
|
|
@@ -495,7 +503,7 @@ export class SandboxBridge {
|
|
|
495
503
|
`[custom-blocks-sdk] host reported init error (${message.error.code}): ${message.error.message}`,
|
|
496
504
|
)
|
|
497
505
|
if (this.rejectInit) {
|
|
498
|
-
this.rejectInit(new
|
|
506
|
+
this.rejectInit(new CustomBlockInitializationError(message.error))
|
|
499
507
|
this.resolveInit = undefined
|
|
500
508
|
this.rejectInit = undefined
|
|
501
509
|
}
|
|
@@ -505,10 +513,28 @@ export class SandboxBridge {
|
|
|
505
513
|
const { blockId, parent, page } = message
|
|
506
514
|
|
|
507
515
|
this.latestDataSourceBindings = message.dataSources.bindings
|
|
508
|
-
const dataSources =
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
516
|
+
const dataSources = postResult
|
|
517
|
+
? resolveDataSources({
|
|
518
|
+
manifest: message.manifest,
|
|
519
|
+
dataSourceBindings: this.latestDataSourceBindings,
|
|
520
|
+
})
|
|
521
|
+
: resolveMockDataSources(this.latestDataSourceBindings)
|
|
522
|
+
const bindingError = getInitBindingError(message.manifest, dataSources)
|
|
523
|
+
if (postResult && bindingError !== undefined) {
|
|
524
|
+
const result: InitResultMessage = {
|
|
525
|
+
type: "initResult",
|
|
526
|
+
initializationId: message.initializationId,
|
|
527
|
+
status: "error",
|
|
528
|
+
error: bindingError,
|
|
529
|
+
}
|
|
530
|
+
this.postToHost(result)
|
|
531
|
+
if (this.rejectInit) {
|
|
532
|
+
this.rejectInit(new CustomBlockInitializationError(bindingError))
|
|
533
|
+
this.resolveInit = undefined
|
|
534
|
+
this.rejectInit = undefined
|
|
535
|
+
}
|
|
536
|
+
return
|
|
537
|
+
}
|
|
512
538
|
this.hostState = {
|
|
513
539
|
status: "initialized",
|
|
514
540
|
theme: message.theme,
|
|
@@ -516,15 +542,24 @@ export class SandboxBridge {
|
|
|
516
542
|
parent,
|
|
517
543
|
page,
|
|
518
544
|
currentUser: message.currentUser,
|
|
545
|
+
manifest: message.manifest,
|
|
519
546
|
dataSources,
|
|
520
547
|
dataSourceState: {},
|
|
521
548
|
}
|
|
522
549
|
this.notify()
|
|
550
|
+
if (postResult) {
|
|
551
|
+
const result: InitResultMessage = {
|
|
552
|
+
type: "initResult",
|
|
553
|
+
initializationId: message.initializationId,
|
|
554
|
+
status: "success",
|
|
555
|
+
}
|
|
556
|
+
this.postToHost(result)
|
|
557
|
+
}
|
|
523
558
|
// Resolve the awaitInit promise once. Subsequent `init` messages
|
|
524
559
|
// (the host shouldn't send these, but be tolerant) update state but
|
|
525
560
|
// don't re-resolve.
|
|
526
561
|
if (this.resolveInit) {
|
|
527
|
-
this.resolveInit(
|
|
562
|
+
this.resolveInit()
|
|
528
563
|
this.resolveInit = undefined
|
|
529
564
|
this.rejectInit = undefined
|
|
530
565
|
}
|
|
@@ -558,21 +593,18 @@ export class SandboxBridge {
|
|
|
558
593
|
}
|
|
559
594
|
|
|
560
595
|
const limit = resolveDataSourceQueryLimit(options.limit)
|
|
561
|
-
const
|
|
596
|
+
const subscriptionId = makeDataSourceSubscriptionId({
|
|
562
597
|
key,
|
|
563
598
|
})
|
|
564
599
|
|
|
565
600
|
if (
|
|
566
601
|
currentState.isLoading &&
|
|
567
|
-
currentState.
|
|
602
|
+
currentState.subscriptionId === subscriptionId &&
|
|
568
603
|
currentState.latestLimit === limit
|
|
569
604
|
) {
|
|
570
605
|
return
|
|
571
606
|
}
|
|
572
607
|
|
|
573
|
-
const requestId = `custom-block-query-${this.nextRequestId}`
|
|
574
|
-
this.nextRequestId += 1
|
|
575
|
-
|
|
576
608
|
this.hostState = {
|
|
577
609
|
...this.hostState,
|
|
578
610
|
dataSourceState: {
|
|
@@ -581,8 +613,7 @@ export class SandboxBridge {
|
|
|
581
613
|
...currentState,
|
|
582
614
|
isLoading: true,
|
|
583
615
|
error: undefined,
|
|
584
|
-
|
|
585
|
-
latestSnapshotId: snapshotId,
|
|
616
|
+
subscriptionId,
|
|
586
617
|
latestLimit: limit,
|
|
587
618
|
},
|
|
588
619
|
},
|
|
@@ -591,8 +622,7 @@ export class SandboxBridge {
|
|
|
591
622
|
|
|
592
623
|
const outbound: QueryDataSourceMessage = {
|
|
593
624
|
type: "queryDataSource",
|
|
594
|
-
|
|
595
|
-
snapshotId,
|
|
625
|
+
subscriptionId,
|
|
596
626
|
dataSourceId: dataSource.collectionPointer.id,
|
|
597
627
|
limit,
|
|
598
628
|
}
|
|
@@ -615,8 +645,7 @@ export class SandboxBridge {
|
|
|
615
645
|
...currentState,
|
|
616
646
|
isLoading: false,
|
|
617
647
|
error,
|
|
618
|
-
|
|
619
|
-
latestSnapshotId: undefined,
|
|
648
|
+
subscriptionId: undefined,
|
|
620
649
|
latestLimit: undefined,
|
|
621
650
|
},
|
|
622
651
|
},
|
|
@@ -907,11 +936,56 @@ function resolveDataSourceQueryLimit(limit: number | undefined): number {
|
|
|
907
936
|
return limit
|
|
908
937
|
}
|
|
909
938
|
|
|
910
|
-
function
|
|
939
|
+
function makeDataSourceSubscriptionId(args: { key: string }): string {
|
|
911
940
|
const { key } = args
|
|
912
941
|
return `data-source:${encodeURIComponent(key)}`
|
|
913
942
|
}
|
|
914
943
|
|
|
944
|
+
function getInitBindingError(
|
|
945
|
+
manifest: CustomBlockManifest,
|
|
946
|
+
dataSources: NotionDataSource[],
|
|
947
|
+
): CustomBlockInitResultErrorInfo | undefined {
|
|
948
|
+
for (const [dataSourceKey, manifestDataSource] of Object.entries(
|
|
949
|
+
manifest.dataSources,
|
|
950
|
+
)) {
|
|
951
|
+
const dataSource = dataSources.find(entry => entry.key === dataSourceKey)
|
|
952
|
+
if (
|
|
953
|
+
dataSource?.collectionPointer === undefined ||
|
|
954
|
+
dataSource.collectionSchema === undefined
|
|
955
|
+
) {
|
|
956
|
+
return {
|
|
957
|
+
code: "invalid_init_bindings",
|
|
958
|
+
message: `Host did not provide a complete binding for data source "${dataSourceKey}".`,
|
|
959
|
+
isRetryable: false,
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
for (const propertyKey of Object.keys(
|
|
963
|
+
manifestDataSource.properties ?? {},
|
|
964
|
+
)) {
|
|
965
|
+
if (dataSource.propertyIdsByKey[propertyKey] === undefined) {
|
|
966
|
+
return {
|
|
967
|
+
code: "invalid_init_bindings",
|
|
968
|
+
message: `Host did not provide a valid binding for property "${dataSourceKey}.${propertyKey}".`,
|
|
969
|
+
isRetryable: false,
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return undefined
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function resolveMockDataSources(
|
|
978
|
+
bindings: NotionDataSourceBindings,
|
|
979
|
+
): NotionDataSource[] {
|
|
980
|
+
return Object.entries(bindings).map(([key, binding]) => ({
|
|
981
|
+
key,
|
|
982
|
+
collectionPointer: binding.collectionPointer,
|
|
983
|
+
collectionSchema: binding.collectionSchema,
|
|
984
|
+
propertyIdsByKey: { ...(binding.propertyIdsByKey ?? {}) },
|
|
985
|
+
propertySchemasById: binding.collectionSchema?.propertiesById ?? {},
|
|
986
|
+
}))
|
|
987
|
+
}
|
|
988
|
+
|
|
915
989
|
function formatInvalidHostReason(
|
|
916
990
|
incomingType: string | undefined,
|
|
917
991
|
issues: readonly v.BaseIssue<unknown>[],
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
} from "./dataSource.js"
|
|
6
6
|
|
|
7
7
|
type ResolveDataSourcesArgs = {
|
|
8
|
-
manifest: CustomBlockManifest
|
|
8
|
+
manifest: CustomBlockManifest
|
|
9
9
|
dataSourceBindings: NotionDataSourceBindings
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -13,23 +13,12 @@ type ResolveDataSourcesArgs = {
|
|
|
13
13
|
* Builds the public {@link NotionDataSource} list the SDK exposes to consumers.
|
|
14
14
|
*
|
|
15
15
|
* Combines the host-supplied bindings (collection pointers + schemas) with the
|
|
16
|
-
* manifest's declared data-source keys.
|
|
17
|
-
*
|
|
18
|
-
* but does not infer a missing mapping.
|
|
16
|
+
* manifest's declared data-source keys. The host owns every mapping; the SDK
|
|
17
|
+
* validates supplied IDs but never infers omitted bindings.
|
|
19
18
|
*/
|
|
20
19
|
export function resolveDataSources(
|
|
21
20
|
args: ResolveDataSourcesArgs,
|
|
22
21
|
): NotionDataSource[] {
|
|
23
|
-
if (args.manifest === null) {
|
|
24
|
-
return Object.entries(args.dataSourceBindings).map(([key, binding]) => ({
|
|
25
|
-
key,
|
|
26
|
-
collectionPointer: binding.collectionPointer,
|
|
27
|
-
collectionSchema: binding.collectionSchema,
|
|
28
|
-
propertyIdsByKey: { ...(binding.propertyIdsByKey ?? {}) },
|
|
29
|
-
propertySchemasById: binding.collectionSchema?.propertiesById ?? {},
|
|
30
|
-
}))
|
|
31
|
-
}
|
|
32
|
-
|
|
33
22
|
return Object.entries(args.manifest.dataSources).map(
|
|
34
23
|
([key, manifestDataSource]) => {
|
|
35
24
|
const binding = args.dataSourceBindings[key]
|
package/src/bridge/hostState.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import type { NotionDataSourceValue } from "./dataSources/dataSourceValue.js"
|
|
9
9
|
import type { NotionPropertySchema } from "./dataSources/propertySchema.js"
|
|
10
10
|
import type { NotionBlockId } from "./ids.js"
|
|
11
|
+
import type { CustomBlockManifest } from "./manifest.js"
|
|
11
12
|
import type { CustomBlockQueryDataSourceErrorInfo } from "./messages/queryDataSourceResult.js"
|
|
12
13
|
import type { CustomBlockPage, NotionPageId } from "./pages/page.js"
|
|
13
14
|
import type { NotionParent } from "./parent.js"
|
|
@@ -28,6 +29,7 @@ export type InitializedHostState = {
|
|
|
28
29
|
parent: NotionParent
|
|
29
30
|
page: CustomBlockPage
|
|
30
31
|
currentUser: NotionUser
|
|
32
|
+
manifest: CustomBlockManifest
|
|
31
33
|
dataSources: NotionDataSource[]
|
|
32
34
|
dataSourceState: Record<string, DataSourceQueryState>
|
|
33
35
|
}
|
|
@@ -38,8 +40,7 @@ export type DataSourceQueryState = {
|
|
|
38
40
|
isLoading: boolean
|
|
39
41
|
hasMore: boolean
|
|
40
42
|
error?: CustomBlockQueryDataSourceErrorInfo
|
|
41
|
-
|
|
42
|
-
latestSnapshotId?: string
|
|
43
|
+
subscriptionId?: string
|
|
43
44
|
latestLimit?: number
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -4,13 +4,13 @@ import { type CustomBlockManifest, manifestSchema } from "./manifest.js"
|
|
|
4
4
|
|
|
5
5
|
const MANIFEST_URL = "custom_blocks.json"
|
|
6
6
|
|
|
7
|
-
export type
|
|
7
|
+
export type CustomBlockConnectErrorCode =
|
|
8
8
|
| "manifest_unavailable"
|
|
9
9
|
| "manifest_invalid"
|
|
10
10
|
| (string & {})
|
|
11
11
|
|
|
12
|
-
export type
|
|
13
|
-
CustomBlockErrorInfo<
|
|
12
|
+
export type CustomBlockConnectError =
|
|
13
|
+
CustomBlockErrorInfo<CustomBlockConnectErrorCode>
|
|
14
14
|
|
|
15
15
|
export type ManifestLoadResult =
|
|
16
16
|
| {
|
|
@@ -19,7 +19,7 @@ export type ManifestLoadResult =
|
|
|
19
19
|
}
|
|
20
20
|
| {
|
|
21
21
|
manifest: null
|
|
22
|
-
error:
|
|
22
|
+
error: CustomBlockConnectError
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -27,7 +27,7 @@ export type ManifestLoadResult =
|
|
|
27
27
|
* A missing manifest means the block has no declared data requirements.
|
|
28
28
|
* Other failures return a structured error so the host can reject the manifest.
|
|
29
29
|
* The SDK validates the manifest locally for author feedback; the host still
|
|
30
|
-
* validates the
|
|
30
|
+
* validates the connect message as the iframe trust boundary.
|
|
31
31
|
*/
|
|
32
32
|
export async function loadManifest(): Promise<ManifestLoadResult> {
|
|
33
33
|
if (typeof fetch !== "function") {
|
|
@@ -48,7 +48,7 @@ export async function loadManifest(): Promise<ManifestLoadResult> {
|
|
|
48
48
|
manifest: null,
|
|
49
49
|
// Consider this retryable because it may have just been a network issue. Because `init`
|
|
50
50
|
// errors are considered terminal, the host should "retry" by reloading the iframe and
|
|
51
|
-
// sending another `
|
|
51
|
+
// sending another `connect` message.
|
|
52
52
|
error: { code: "manifest_unavailable", message, isRetryable: true },
|
|
53
53
|
}
|
|
54
54
|
}
|
package/src/bridge/manifest.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { notionPropertyTypeSchema } from "./dataSources/propertySchema.js"
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* User-authored manifest declaring the data sources the custom block expects.
|
|
6
|
-
* Lives at `custom_blocks.json` in the project root
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Lives at `custom_blocks.json` in the project root. The sandbox may send it in
|
|
7
|
+
* `connect`, and the host returns the authoritative manifest in `init`. The
|
|
8
|
+
* `notionCustomBlock` Vite plugin from
|
|
9
9
|
* `@notionhq/custom-blocks/vite` wires the JSON file into the dev server and
|
|
10
10
|
* the build output.
|
|
11
11
|
*/
|