@notionhq/custom-blocks 0.0.63 → 0.0.65
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 +16 -16
- package/README.md +2 -2
- package/bin/cli/create.js +3 -3
- package/dist/bridge/SandboxBridge.js +17 -17
- package/dist/bridge/loadManifest.d.ts +2 -7
- package/dist/bridge/loadManifest.d.ts.map +1 -1
- package/dist/bridge/loadManifest.js +36 -18
- package/dist/bridge/messages/ready.d.ts +1 -0
- package/dist/bridge/messages/ready.d.ts.map +1 -1
- package/dist/bridge/messages/ready.js +8 -2
- package/dist/bridge/messages/sandboxToHost.d.ts +1 -0
- package/dist/bridge/messages/sandboxToHost.d.ts.map +1 -1
- package/dist/host/createCustomBlockHost.d.ts.map +1 -1
- package/dist/host/createCustomBlockHost.js +24 -13
- package/dist/host/lifecycle/initErrors.d.ts +4 -1
- package/dist/host/lifecycle/initErrors.d.ts.map +1 -1
- package/dist/host/lifecycle/initErrors.js +16 -5
- package/dist/host/lifecycle/ready.d.ts +1 -7
- package/dist/host/lifecycle/ready.d.ts.map +1 -1
- package/dist/host/lifecycle/ready.js +1 -22
- package/dist/init.js +1 -1
- package/dist/react/NotionCustomBlock.css +47 -47
- package/dist/react/NotionCustomBlock.js +4 -4
- package/dist/utils.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/docs/block-location.md +1 -1
- package/docs/manifest.md +2 -2
- package/docs/pages.md +1 -1
- package/package.json +1 -1
- package/src/bridge/SandboxBridge.ts +17 -17
- package/src/bridge/loadManifest.ts +44 -23
- package/src/bridge/messages/ready.ts +9 -2
- package/src/host/createCustomBlockHost.ts +30 -14
- package/src/host/lifecycle/initErrors.ts +19 -4
- package/src/host/lifecycle/ready.ts +1 -34
- package/src/init.ts +1 -1
- package/src/react/NotionCustomBlock.css +47 -47
- package/src/react/NotionCustomBlock.tsx +14 -10
- package/src/utils.ts +1 -1
- package/src/version.ts +1 -1
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
.
|
|
2
|
-
--
|
|
3
|
-
--
|
|
4
|
-
--
|
|
1
|
+
.custom-blocks-standalone-banner {
|
|
2
|
+
--custom-blocks-standalone-banner-background: #fff8e1;
|
|
3
|
+
--custom-blocks-standalone-banner-border: #f0d77b;
|
|
4
|
+
--custom-blocks-standalone-banner-text: #5d4200;
|
|
5
5
|
|
|
6
6
|
padding: 8px 12px;
|
|
7
|
-
background: var(--
|
|
8
|
-
color: var(--
|
|
9
|
-
border-bottom: 1px solid var(--
|
|
7
|
+
background: var(--custom-blocks-standalone-banner-background);
|
|
8
|
+
color: var(--custom-blocks-standalone-banner-text);
|
|
9
|
+
border-bottom: 1px solid var(--custom-blocks-standalone-banner-border);
|
|
10
10
|
font-size: 13px;
|
|
11
11
|
font-family: system-ui, sans-serif;
|
|
12
12
|
line-height: 1.4;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
.
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
15
|
+
.custom-blocks-default-init-error {
|
|
16
|
+
--custom-blocks-init-error-background: #f7f7f5;
|
|
17
|
+
--custom-blocks-init-error-card-background: #ffffff;
|
|
18
|
+
--custom-blocks-init-error-border: #e5e5e1;
|
|
19
|
+
--custom-blocks-init-error-text: #1f1f1d;
|
|
20
|
+
--custom-blocks-init-error-muted-text: #5f5e5b;
|
|
21
|
+
--custom-blocks-init-error-eyebrow-text: #787774;
|
|
22
|
+
--custom-blocks-init-error-pre-background: #f1f1ef;
|
|
23
|
+
--custom-blocks-init-error-pre-text: #37352f;
|
|
24
|
+
--custom-blocks-init-error-shadow: rgba(0, 0, 0, 0.08);
|
|
25
25
|
|
|
26
26
|
box-sizing: border-box;
|
|
27
27
|
min-height: 240px;
|
|
@@ -29,81 +29,81 @@
|
|
|
29
29
|
align-items: center;
|
|
30
30
|
justify-content: center;
|
|
31
31
|
padding: 24px;
|
|
32
|
-
background: var(--
|
|
33
|
-
color: var(--
|
|
32
|
+
background: var(--custom-blocks-init-error-background);
|
|
33
|
+
color: var(--custom-blocks-init-error-text);
|
|
34
34
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
35
35
|
color-scheme: light;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
.
|
|
38
|
+
.custom-blocks-default-init-error__card {
|
|
39
39
|
box-sizing: border-box;
|
|
40
40
|
width: 100%;
|
|
41
41
|
max-width: 420px;
|
|
42
42
|
padding: 20px;
|
|
43
|
-
border: 1px solid var(--
|
|
43
|
+
border: 1px solid var(--custom-blocks-init-error-border);
|
|
44
44
|
border-radius: 12px;
|
|
45
|
-
background: var(--
|
|
46
|
-
box-shadow: 0 8px 24px var(--
|
|
45
|
+
background: var(--custom-blocks-init-error-card-background);
|
|
46
|
+
box-shadow: 0 8px 24px var(--custom-blocks-init-error-shadow);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
.
|
|
49
|
+
.custom-blocks-default-init-error__eyebrow {
|
|
50
50
|
margin-bottom: 8px;
|
|
51
51
|
font-size: 11px;
|
|
52
52
|
font-weight: 600;
|
|
53
53
|
letter-spacing: 0.08em;
|
|
54
54
|
text-transform: uppercase;
|
|
55
|
-
color: var(--
|
|
55
|
+
color: var(--custom-blocks-init-error-eyebrow-text);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
.
|
|
58
|
+
.custom-blocks-default-init-error__title {
|
|
59
59
|
margin: 0;
|
|
60
60
|
font-size: 18px;
|
|
61
61
|
line-height: 1.3;
|
|
62
62
|
font-weight: 650;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
.
|
|
65
|
+
.custom-blocks-default-init-error__body {
|
|
66
66
|
margin: 8px 0 0;
|
|
67
67
|
font-size: 14px;
|
|
68
68
|
line-height: 1.5;
|
|
69
|
-
color: var(--
|
|
69
|
+
color: var(--custom-blocks-init-error-muted-text);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
.
|
|
72
|
+
.custom-blocks-default-init-error__details {
|
|
73
73
|
margin-top: 14px;
|
|
74
74
|
font-size: 12px;
|
|
75
|
-
color: var(--
|
|
75
|
+
color: var(--custom-blocks-init-error-muted-text);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
.
|
|
78
|
+
.custom-blocks-default-init-error__pre {
|
|
79
79
|
margin: 8px 0 0;
|
|
80
80
|
padding: 10px;
|
|
81
81
|
overflow: auto;
|
|
82
82
|
border-radius: 8px;
|
|
83
|
-
background: var(--
|
|
84
|
-
color: var(--
|
|
83
|
+
background: var(--custom-blocks-init-error-pre-background);
|
|
84
|
+
color: var(--custom-blocks-init-error-pre-text);
|
|
85
85
|
font-size: 12px;
|
|
86
86
|
line-height: 1.4;
|
|
87
87
|
white-space: pre-wrap;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
@media (prefers-color-scheme: dark) {
|
|
91
|
-
.
|
|
92
|
-
--
|
|
93
|
-
--
|
|
94
|
-
--
|
|
91
|
+
.custom-blocks-standalone-banner {
|
|
92
|
+
--custom-blocks-standalone-banner-background: #3b2f0b;
|
|
93
|
+
--custom-blocks-standalone-banner-border: #6f5713;
|
|
94
|
+
--custom-blocks-standalone-banner-text: #f5d36c;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
.
|
|
98
|
-
--
|
|
99
|
-
--
|
|
100
|
-
--
|
|
101
|
-
--
|
|
102
|
-
--
|
|
103
|
-
--
|
|
104
|
-
--
|
|
105
|
-
--
|
|
106
|
-
--
|
|
97
|
+
.custom-blocks-default-init-error {
|
|
98
|
+
--custom-blocks-init-error-background: #191918;
|
|
99
|
+
--custom-blocks-init-error-card-background: #20201f;
|
|
100
|
+
--custom-blocks-init-error-border: #373633;
|
|
101
|
+
--custom-blocks-init-error-text: #f1f1ef;
|
|
102
|
+
--custom-blocks-init-error-muted-text: #b9b8b3;
|
|
103
|
+
--custom-blocks-init-error-eyebrow-text: #9b9a97;
|
|
104
|
+
--custom-blocks-init-error-pre-background: #2a2a28;
|
|
105
|
+
--custom-blocks-init-error-pre-text: #e8e8e4;
|
|
106
|
+
--custom-blocks-init-error-shadow: rgba(0, 0, 0, 0.35);
|
|
107
107
|
|
|
108
108
|
color-scheme: dark;
|
|
109
109
|
}
|
|
@@ -45,7 +45,7 @@ export function NotionCustomBlock({ children, timeoutMs, fallback = null, errorF
|
|
|
45
45
|
if (!isStandalone) {
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
|
-
console.warn(`[
|
|
48
|
+
console.warn(`[custom-blocks-sdk] ${init.error?.message}`);
|
|
49
49
|
seedStandalonePreviewState();
|
|
50
50
|
}, [isStandalone, init.error]);
|
|
51
51
|
if (debugOpen) {
|
|
@@ -65,7 +65,7 @@ export function NotionCustomBlock({ children, timeoutMs, fallback = null, errorF
|
|
|
65
65
|
if (host.status !== "initialized") {
|
|
66
66
|
return _jsx(_Fragment, { children: fallback });
|
|
67
67
|
}
|
|
68
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { role: "status", className: "
|
|
68
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { role: "status", className: "custom-blocks-standalone-banner", children: "Notion host not detected \u2014 running in standalone preview. SDK hooks return placeholder values until embedded in Notion." }), children] }));
|
|
69
69
|
}
|
|
70
70
|
if (!init.isLoaded) {
|
|
71
71
|
return _jsx(_Fragment, { children: fallback });
|
|
@@ -75,7 +75,7 @@ export function NotionCustomBlock({ children, timeoutMs, fallback = null, errorF
|
|
|
75
75
|
// TODO(custom-blocks): Align the styling of this with the host-side error states.
|
|
76
76
|
function DefaultInitErrorFallback({ error }) {
|
|
77
77
|
useEffect(() => {
|
|
78
|
-
console.warn(`[
|
|
78
|
+
console.warn(`[custom-blocks-sdk] Custom block init failed: ${error.message}`);
|
|
79
79
|
}, [error]);
|
|
80
|
-
return (_jsx("div", { role: "alert", className: "
|
|
80
|
+
return (_jsx("div", { role: "alert", className: "custom-blocks-default-init-error", children: _jsxs("div", { className: "custom-blocks-default-init-error__card", children: [_jsx("div", { className: "custom-blocks-default-init-error__eyebrow", children: "Custom block" }), _jsx("h2", { className: "custom-blocks-default-init-error__title", children: "Couldn't connect to Notion" }), _jsx("p", { className: "custom-blocks-default-init-error__body", children: "This custom block loaded, but the setup handshake didn't finish. Try again from Notion. If this keeps happening, check the browser console for SDK details." }), _jsxs("details", { className: "custom-blocks-default-init-error__details", children: [_jsx("summary", { children: "Developer details" }), _jsx("pre", { className: "custom-blocks-default-init-error__pre", children: error.message })] })] }) }));
|
|
81
81
|
}
|
package/dist/utils.js
CHANGED
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
* are covered when using discriminated unions.
|
|
7
7
|
*/
|
|
8
8
|
export function unreachable(value) {
|
|
9
|
-
throw new Error(`[
|
|
9
|
+
throw new Error(`[custom-blocks-sdk] Unexpected value encountered: ${JSON.stringify(value)}`);
|
|
10
10
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* This checked-in value intentionally stays generic for local development. The SDK publish build
|
|
6
6
|
* overwrites the compiled runtime module with package.json's semver before publishing.
|
|
7
7
|
*/
|
|
8
|
-
export declare const
|
|
8
|
+
export declare const CUSTOM_BLOCKS_SDK_VERSION: string;
|
|
9
9
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/version.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/version.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,EAAE,MAAgB,CAAA"}
|
package/dist/version.js
CHANGED
package/docs/block-location.md
CHANGED
|
@@ -46,7 +46,7 @@ For non-React renderers, use `customBlock.getParent()` after `initCustomBlock()`
|
|
|
46
46
|
To identify custom blocks in agent-owned instruction content:
|
|
47
47
|
|
|
48
48
|
```tsx
|
|
49
|
-
import { useParent } from "
|
|
49
|
+
import { useParent } from "@notionhq/custom-blocks"
|
|
50
50
|
|
|
51
51
|
export function AgentInstructionBadge() {
|
|
52
52
|
const parent = useParent()
|
package/docs/manifest.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Manifest
|
|
2
2
|
|
|
3
|
-
A custom
|
|
3
|
+
A custom block declares its required data sources in `custom_blocks.json` at the project root. Notion uses the manifest to know what semantic keys the block expects, what shape each property should be, and what to show when an admin is configuring the block.
|
|
4
4
|
|
|
5
5
|
```json
|
|
6
6
|
{
|
|
@@ -18,7 +18,7 @@ A custom view declares its required data sources in `custom_blocks.json` at the
|
|
|
18
18
|
}
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
`initCustomBlock()` fetches
|
|
21
|
+
`initCustomBlock()` fetches the manifest and forwards it with `ready`. 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 sends `ready` with `status: "success"` and `manifest: null`, which means the block has no declared data requirements. If the file is unavailable for another reason or invalid, the SDK sends `ready` with `status: "error"` and an `error` payload. Hosts should reject `init` by forwarding that full error payload unchanged.
|
|
22
22
|
|
|
23
23
|
## Vite plugin
|
|
24
24
|
|
package/docs/pages.md
CHANGED
|
@@ -53,7 +53,7 @@ type CreatePageParent =
|
|
|
53
53
|
| { type: "data_source_key"; key: string };
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
`type: "data_source_key"` is the recommended form inside a custom
|
|
56
|
+
`type: "data_source_key"` is the recommended form inside a custom block. Pass the semantic key you declared in `custom_blocks.json` (e.g. `"tasks"`) and the SDK looks up the corresponding data source for you. The other two variants exist for the rarer case where you already have a raw Notion ID in hand.
|
|
57
57
|
|
|
58
58
|
### Property keys
|
|
59
59
|
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
UseDataSourceOptions,
|
|
14
14
|
} from "../types.js"
|
|
15
15
|
import { unreachable } from "../utils.js"
|
|
16
|
-
import {
|
|
16
|
+
import { CUSTOM_BLOCKS_SDK_VERSION } from "../version.js"
|
|
17
17
|
import type {
|
|
18
18
|
NotionDataSource,
|
|
19
19
|
NotionDataSourceBindings,
|
|
@@ -167,7 +167,7 @@ export class SandboxBridge {
|
|
|
167
167
|
return
|
|
168
168
|
}
|
|
169
169
|
if (this.hasSentReady) {
|
|
170
|
-
console.warn("[
|
|
170
|
+
console.warn("[custom-blocks-sdk] ignoring duplicate ready message")
|
|
171
171
|
return
|
|
172
172
|
}
|
|
173
173
|
const { manifest, error } = manifestResult
|
|
@@ -179,21 +179,21 @@ export class SandboxBridge {
|
|
|
179
179
|
type: "ready",
|
|
180
180
|
status: "error",
|
|
181
181
|
bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
|
|
182
|
-
sdkVersion:
|
|
182
|
+
sdkVersion: CUSTOM_BLOCKS_SDK_VERSION,
|
|
183
183
|
error,
|
|
184
184
|
}
|
|
185
185
|
: {
|
|
186
186
|
type: "ready",
|
|
187
187
|
status: "success",
|
|
188
188
|
bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
|
|
189
|
-
sdkVersion:
|
|
189
|
+
sdkVersion: CUSTOM_BLOCKS_SDK_VERSION,
|
|
190
190
|
manifest,
|
|
191
191
|
}
|
|
192
192
|
this.postToHost(readyMessage)
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
private postToHost(message: unknown) {
|
|
196
|
-
console.debug("[
|
|
196
|
+
console.debug("[custom-blocks-sdk] outbound postMessage", message)
|
|
197
197
|
this.logMessage("sent", message)
|
|
198
198
|
window.parent.postMessage(message, "*")
|
|
199
199
|
}
|
|
@@ -205,7 +205,7 @@ export class SandboxBridge {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
private handleMessage = (event: MessageEvent) => {
|
|
208
|
-
console.debug("[
|
|
208
|
+
console.debug("[custom-blocks-sdk] incoming postMessage", {
|
|
209
209
|
data: event.data,
|
|
210
210
|
fromParent: event.source === window.parent,
|
|
211
211
|
})
|
|
@@ -217,7 +217,7 @@ export class SandboxBridge {
|
|
|
217
217
|
const parsed = v.safeParse(hostToSandboxMessageSchema, event.data)
|
|
218
218
|
if (!parsed.success) {
|
|
219
219
|
console.warn(
|
|
220
|
-
"[
|
|
220
|
+
"[custom-blocks-sdk] ignoring malformed host message",
|
|
221
221
|
parsed.issues,
|
|
222
222
|
)
|
|
223
223
|
const incomingType = readIncomingType(event.data)
|
|
@@ -245,7 +245,7 @@ export class SandboxBridge {
|
|
|
245
245
|
// failure on its own.
|
|
246
246
|
if (message.type === "invalidSandboxMessage") {
|
|
247
247
|
console.warn(
|
|
248
|
-
"[
|
|
248
|
+
"[custom-blocks-sdk] host reported invalid sandbox message:",
|
|
249
249
|
message.reason,
|
|
250
250
|
)
|
|
251
251
|
return
|
|
@@ -263,7 +263,7 @@ export class SandboxBridge {
|
|
|
263
263
|
const hostState = this.hostState
|
|
264
264
|
|
|
265
265
|
if (hostState.status !== "initialized") {
|
|
266
|
-
console.warn(`[
|
|
266
|
+
console.warn(`[custom-blocks-sdk] ignoring ${message.type} before init`)
|
|
267
267
|
return
|
|
268
268
|
}
|
|
269
269
|
|
|
@@ -340,7 +340,7 @@ export class SandboxBridge {
|
|
|
340
340
|
: { status: "error", error: message.error }
|
|
341
341
|
if (!this.pendingCreatePage.resolve(message.requestId, result)) {
|
|
342
342
|
console.warn(
|
|
343
|
-
`[
|
|
343
|
+
`[custom-blocks-sdk] createPageResult for unknown requestId ${message.requestId}`,
|
|
344
344
|
)
|
|
345
345
|
}
|
|
346
346
|
return
|
|
@@ -353,7 +353,7 @@ export class SandboxBridge {
|
|
|
353
353
|
: { status: "error", error: message.error }
|
|
354
354
|
if (!this.pendingGetPage.resolve(message.requestId, result)) {
|
|
355
355
|
console.warn(
|
|
356
|
-
`[
|
|
356
|
+
`[custom-blocks-sdk] getPageResult for unknown requestId ${message.requestId}`,
|
|
357
357
|
)
|
|
358
358
|
}
|
|
359
359
|
return
|
|
@@ -366,7 +366,7 @@ export class SandboxBridge {
|
|
|
366
366
|
: { status: "error", error: message.error }
|
|
367
367
|
if (!this.pendingGetUser.resolve(message.requestId, result)) {
|
|
368
368
|
console.warn(
|
|
369
|
-
`[
|
|
369
|
+
`[custom-blocks-sdk] getUserResult for unknown requestId ${message.requestId}`,
|
|
370
370
|
)
|
|
371
371
|
}
|
|
372
372
|
return
|
|
@@ -379,7 +379,7 @@ export class SandboxBridge {
|
|
|
379
379
|
: { status: "error", error: message.error }
|
|
380
380
|
if (!this.pendingListUsers.resolve(message.requestId, result)) {
|
|
381
381
|
console.warn(
|
|
382
|
-
`[
|
|
382
|
+
`[custom-blocks-sdk] listUsersResult for unknown requestId ${message.requestId}`,
|
|
383
383
|
)
|
|
384
384
|
}
|
|
385
385
|
return
|
|
@@ -392,7 +392,7 @@ export class SandboxBridge {
|
|
|
392
392
|
: { status: "error", error: message.error }
|
|
393
393
|
if (!this.pendingUpdatePage.resolve(message.requestId, result)) {
|
|
394
394
|
console.warn(
|
|
395
|
-
`[
|
|
395
|
+
`[custom-blocks-sdk] updatePageResult for unknown requestId ${message.requestId}`,
|
|
396
396
|
)
|
|
397
397
|
}
|
|
398
398
|
return
|
|
@@ -492,7 +492,7 @@ export class SandboxBridge {
|
|
|
492
492
|
// the browser console either way. We deliberately don't NACK the host or post anything
|
|
493
493
|
// back over the bridge since the host has already given up.
|
|
494
494
|
console.error(
|
|
495
|
-
`[
|
|
495
|
+
`[custom-blocks-sdk] host reported init error (${message.error.code}): ${message.error.message}`,
|
|
496
496
|
)
|
|
497
497
|
if (this.rejectInit) {
|
|
498
498
|
this.rejectInit(new CustomBlockInitError(message.error))
|
|
@@ -900,13 +900,13 @@ function resolveDataSourceQueryLimit(limit: number | undefined): number {
|
|
|
900
900
|
}
|
|
901
901
|
if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1) {
|
|
902
902
|
console.warn(
|
|
903
|
-
`[
|
|
903
|
+
`[custom-blocks-sdk] useDataSource limit must be a positive integer; using ${DEFAULT_DATA_SOURCE_QUERY_LIMIT}.`,
|
|
904
904
|
)
|
|
905
905
|
return DEFAULT_DATA_SOURCE_QUERY_LIMIT
|
|
906
906
|
}
|
|
907
907
|
if (limit > MAX_DATA_SOURCE_QUERY_LIMIT) {
|
|
908
908
|
console.warn(
|
|
909
|
-
`[
|
|
909
|
+
`[custom-blocks-sdk] useDataSource limit is capped at ${MAX_DATA_SOURCE_QUERY_LIMIT}.`,
|
|
910
910
|
)
|
|
911
911
|
return MAX_DATA_SOURCE_QUERY_LIMIT
|
|
912
912
|
}
|
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
import * as v from "valibot"
|
|
2
|
+
import type { CustomBlockErrorInfo } from "./errors.js"
|
|
2
3
|
import { type CustomBlockManifest, manifestSchema } from "./manifest.js"
|
|
3
4
|
|
|
4
5
|
const MANIFEST_URL = "custom_blocks.json"
|
|
5
6
|
|
|
6
|
-
// The schema accepts any string code. The type lists known codes for
|
|
7
|
-
// autocomplete, with an open string fallback for newer senders.
|
|
8
|
-
export const customBlockReadyErrorCodeSchema = v.string()
|
|
9
|
-
|
|
10
7
|
export type CustomBlockReadyErrorCode =
|
|
11
8
|
| "manifest_unavailable"
|
|
12
9
|
| "manifest_invalid"
|
|
13
10
|
| (string & {})
|
|
14
11
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
message: v.string(),
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
export type CustomBlockReadyError = v.InferOutput<
|
|
21
|
-
typeof customBlockReadyErrorSchema
|
|
22
|
-
>
|
|
12
|
+
export type CustomBlockReadyError =
|
|
13
|
+
CustomBlockErrorInfo<CustomBlockReadyErrorCode>
|
|
23
14
|
|
|
24
15
|
export type ManifestLoadResult =
|
|
25
16
|
| {
|
|
@@ -41,10 +32,10 @@ export type ManifestLoadResult =
|
|
|
41
32
|
export async function loadManifest(): Promise<ManifestLoadResult> {
|
|
42
33
|
if (typeof fetch !== "function") {
|
|
43
34
|
const message = `No fetch API available; cannot load ${MANIFEST_URL}.`
|
|
44
|
-
console.warn(`[
|
|
35
|
+
console.warn(`[custom-blocks-sdk] ${message}`)
|
|
45
36
|
return {
|
|
46
37
|
manifest: null,
|
|
47
|
-
error: { code: "manifest_unavailable", message },
|
|
38
|
+
error: { code: "manifest_unavailable", message, isRetryable: true },
|
|
48
39
|
}
|
|
49
40
|
}
|
|
50
41
|
let response: Response
|
|
@@ -52,38 +43,68 @@ export async function loadManifest(): Promise<ManifestLoadResult> {
|
|
|
52
43
|
response = await fetch(MANIFEST_URL, { credentials: "omit" })
|
|
53
44
|
} catch (error) {
|
|
54
45
|
const message = `Could not fetch ${MANIFEST_URL}.`
|
|
55
|
-
console.warn(`[
|
|
46
|
+
console.warn(`[custom-blocks-sdk] ${message}`, error)
|
|
56
47
|
return {
|
|
57
48
|
manifest: null,
|
|
58
|
-
|
|
49
|
+
// Consider this retryable because it may have just been a network issue. Because `init`
|
|
50
|
+
// errors are considered terminal, the host should "retry" by reloading the iframe and
|
|
51
|
+
// sending another `ready` message.
|
|
52
|
+
error: { code: "manifest_unavailable", message, isRetryable: true },
|
|
59
53
|
}
|
|
60
54
|
}
|
|
61
55
|
if (response.status === 404) {
|
|
62
56
|
const message = `No manifest found at ${MANIFEST_URL} (status ${response.status}).`
|
|
63
|
-
console.warn(`[
|
|
57
|
+
console.warn(`[custom-blocks-sdk] ${message}`)
|
|
64
58
|
return { manifest: null }
|
|
65
59
|
}
|
|
66
60
|
if (!response.ok) {
|
|
67
61
|
const message = `Could not fetch ${MANIFEST_URL} (status ${response.status}).`
|
|
68
|
-
console.warn(`[
|
|
62
|
+
console.warn(`[custom-blocks-sdk] ${message}`)
|
|
69
63
|
return {
|
|
70
64
|
manifest: null,
|
|
71
|
-
error: {
|
|
65
|
+
error: {
|
|
66
|
+
code: "manifest_unavailable",
|
|
67
|
+
message,
|
|
68
|
+
isRetryable: isRetryableManifestHttpStatus(response.status),
|
|
69
|
+
},
|
|
72
70
|
}
|
|
73
71
|
}
|
|
72
|
+
|
|
74
73
|
let json: unknown
|
|
75
74
|
try {
|
|
76
75
|
json = await response.json()
|
|
77
76
|
} catch (error) {
|
|
78
77
|
const message = `Manifest at ${MANIFEST_URL} was not valid JSON.`
|
|
79
|
-
console.warn(`[
|
|
80
|
-
return {
|
|
78
|
+
console.warn(`[custom-blocks-sdk] ${message}`, error)
|
|
79
|
+
return {
|
|
80
|
+
manifest: null,
|
|
81
|
+
// Consider invalid manifests as unrecoverable errors.
|
|
82
|
+
error: { code: "manifest_invalid", message, isRetryable: false },
|
|
83
|
+
}
|
|
81
84
|
}
|
|
85
|
+
|
|
82
86
|
const parsed = v.safeParse(manifestSchema, json)
|
|
83
87
|
if (!parsed.success) {
|
|
84
88
|
const message = `Manifest at ${MANIFEST_URL} did not match schema.`
|
|
85
|
-
console.warn(`[
|
|
86
|
-
return {
|
|
89
|
+
console.warn(`[custom-blocks-sdk] ${message}`, parsed.issues)
|
|
90
|
+
return {
|
|
91
|
+
manifest: null,
|
|
92
|
+
// Consider invalid manifests as unrecoverable errors.
|
|
93
|
+
error: { code: "manifest_invalid", message, isRetryable: false },
|
|
94
|
+
}
|
|
87
95
|
}
|
|
96
|
+
|
|
88
97
|
return { manifest: parsed.output }
|
|
89
98
|
}
|
|
99
|
+
|
|
100
|
+
function isRetryableManifestHttpStatus(status: number): boolean {
|
|
101
|
+
// Consider these statuses retryable because they may reflect transient failures.
|
|
102
|
+
return (
|
|
103
|
+
status === 408 || // Request Timeout
|
|
104
|
+
status === 425 || // Too Early
|
|
105
|
+
status === 500 || // Internal Server Error
|
|
106
|
+
status === 502 || // Bad Gateway
|
|
107
|
+
status === 503 || // Service Unavailable
|
|
108
|
+
status === 504 // Gateway Timeout
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as v from "valibot"
|
|
2
|
-
import { customBlockReadyErrorSchema } from "../loadManifest.js"
|
|
3
2
|
import { manifestSchema } from "../manifest.js"
|
|
4
3
|
|
|
5
4
|
const readyMessageCommonEntries = {
|
|
@@ -17,6 +16,14 @@ const readyMessageCommonEntries = {
|
|
|
17
16
|
sdkVersion: v.string(),
|
|
18
17
|
}
|
|
19
18
|
|
|
19
|
+
// TODO(custom-blocks): Replace with `customBlockErrorInfoSchema` which makes `isRetryable` required
|
|
20
|
+
// once min supported bridge protocol version is bumped to 3.
|
|
21
|
+
const readyErrorSchema = v.object({
|
|
22
|
+
code: v.string(),
|
|
23
|
+
message: v.string(),
|
|
24
|
+
isRetryable: v.optional(v.boolean(), false),
|
|
25
|
+
})
|
|
26
|
+
|
|
20
27
|
/**
|
|
21
28
|
* First message the sandbox sends after mount, kicking off the bridge handshake. The host replies
|
|
22
29
|
* with `init`.
|
|
@@ -33,7 +40,7 @@ export const readyMessageSchema = v.union([
|
|
|
33
40
|
}),
|
|
34
41
|
v.object({
|
|
35
42
|
status: v.literal("error"),
|
|
36
|
-
error:
|
|
43
|
+
error: readyErrorSchema,
|
|
37
44
|
...readyMessageCommonEntries,
|
|
38
45
|
}),
|
|
39
46
|
])
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
} from "../bridge/messages/getUser.js"
|
|
14
14
|
import type { HostToSandboxMessage } from "../bridge/messages/hostToSandbox.js"
|
|
15
15
|
import type {
|
|
16
|
-
|
|
16
|
+
CustomBlockInitErrorInfo,
|
|
17
17
|
InitMessage,
|
|
18
18
|
} from "../bridge/messages/init.js"
|
|
19
19
|
import type {
|
|
@@ -35,7 +35,7 @@ import type { NotionTheme } from "../bridge/theme.js"
|
|
|
35
35
|
import type { NotionUser } from "../bridge/users/user.js"
|
|
36
36
|
import { unreachable } from "../utils.js"
|
|
37
37
|
import { initErrorForFailureReason } from "./lifecycle/initErrors.js"
|
|
38
|
-
import {
|
|
38
|
+
import { isValidBridgeProtocolVersion } from "./lifecycle/ready.js"
|
|
39
39
|
import type {
|
|
40
40
|
CustomBlockHostDataSourcesPayload,
|
|
41
41
|
CustomBlockHostInitialState,
|
|
@@ -161,27 +161,24 @@ export function createCustomBlockHost(
|
|
|
161
161
|
return true
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
function sendInitError(
|
|
164
|
+
function sendInitError(error: CustomBlockInitErrorInfo) {
|
|
165
165
|
if (initStatus !== "pending") {
|
|
166
166
|
return
|
|
167
167
|
}
|
|
168
168
|
initStatus = "error"
|
|
169
169
|
clearNoReadyTimeout()
|
|
170
|
-
const error = initErrorForFailureReason(code)
|
|
171
170
|
post({
|
|
172
171
|
type: "init",
|
|
173
172
|
status: "error",
|
|
174
|
-
error
|
|
175
|
-
...error,
|
|
176
|
-
message: message ?? error.message,
|
|
177
|
-
},
|
|
173
|
+
error,
|
|
178
174
|
})
|
|
179
175
|
}
|
|
180
176
|
|
|
181
177
|
function onIframeLoad() {
|
|
182
178
|
clearNoReadyTimeout()
|
|
183
179
|
noReadyTimeoutId = window.setTimeout(() => {
|
|
184
|
-
|
|
180
|
+
const initError = initErrorForFailureReason("no_ready")
|
|
181
|
+
sendInitError(initError)
|
|
185
182
|
}, noReadyTimeoutMs)
|
|
186
183
|
}
|
|
187
184
|
|
|
@@ -261,14 +258,32 @@ export function createCustomBlockHost(
|
|
|
261
258
|
}
|
|
262
259
|
|
|
263
260
|
function handleReady(message: ReadyMessage) {
|
|
264
|
-
|
|
265
|
-
if (
|
|
266
|
-
|
|
261
|
+
// Verify the bridge protocol version is valid.
|
|
262
|
+
if (!isValidBridgeProtocolVersion(message.bridgeProtocolVersion)) {
|
|
263
|
+
const initError = initErrorForFailureReason("invalid_protocol_version", {
|
|
264
|
+
currentBridgeProtocolVersion: message.bridgeProtocolVersion,
|
|
265
|
+
})
|
|
266
|
+
sendInitError(initError)
|
|
267
267
|
return
|
|
268
268
|
}
|
|
269
|
-
if (message.
|
|
269
|
+
if (message.bridgeProtocolVersion < minBridgeProtocolVersion) {
|
|
270
|
+
const initError = initErrorForFailureReason(
|
|
271
|
+
"unsupported_protocol_version",
|
|
272
|
+
{
|
|
273
|
+
currentBridgeProtocolVersion: message.bridgeProtocolVersion,
|
|
274
|
+
minBridgeProtocolVersion,
|
|
275
|
+
},
|
|
276
|
+
)
|
|
277
|
+
sendInitError(initError)
|
|
270
278
|
return
|
|
271
279
|
}
|
|
280
|
+
|
|
281
|
+
// If the sandbox reported an error during `ready`, return it as an `init` error.
|
|
282
|
+
if (message.status === "error") {
|
|
283
|
+
sendInitError(message.error)
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
272
287
|
if (sendInit(message)) {
|
|
273
288
|
onReady?.(message)
|
|
274
289
|
}
|
|
@@ -286,7 +301,8 @@ export function createCustomBlockHost(
|
|
|
286
301
|
const response = getInvalidSandboxMessageResponse(data)
|
|
287
302
|
|
|
288
303
|
if (initStatus === "pending" && response.incomingType === "ready") {
|
|
289
|
-
|
|
304
|
+
const initError = initErrorForFailureReason("invalid_ready")
|
|
305
|
+
sendInitError(initError)
|
|
290
306
|
} else if (response.nack !== undefined) {
|
|
291
307
|
post(response.nack)
|
|
292
308
|
}
|