@notionhq/custom-blocks 0.0.77 → 0.0.79
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 -34
- package/README.md +1 -1
- package/bin/src/bridge/manifest.js +3 -3
- package/dist/bridge/SandboxBridge.d.ts +8 -13
- package/dist/bridge/SandboxBridge.d.ts.map +1 -1
- package/dist/bridge/SandboxBridge.js +110 -39
- 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 +2 -0
- 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} +14 -12
- package/dist/bridge/messages/connect.d.ts.map +1 -0
- package/dist/bridge/messages/connect.js +38 -0
- package/dist/bridge/messages/hostToSandbox.d.ts +21 -0
- package/dist/bridge/messages/hostToSandbox.d.ts.map +1 -1
- 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/sandboxToHost.d.ts +21 -6
- 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 +7 -4
- package/dist/host/createCustomBlockHost.d.ts.map +1 -1
- package/dist/host/createCustomBlockHost.js +215 -38
- 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/{ready.d.ts → protocolVersion.d.ts} +1 -1
- package/dist/host/lifecycle/protocolVersion.d.ts.map +1 -0
- 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.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 +10 -8
- 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 +138 -44
- package/src/bridge/dataSources/resolve.ts +3 -14
- package/src/bridge/hostState.ts +2 -0
- package/src/bridge/loadManifest.ts +6 -6
- package/src/bridge/manifest.ts +3 -3
- package/src/bridge/messages/connect.ts +42 -0
- package/src/bridge/messages/init.ts +24 -10
- package/src/bridge/messages/initResult.ts +37 -0
- 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 +265 -45
- 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.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/dist/bridge/messages/ready.js +0 -43
- package/dist/host/lifecycle/ready.d.ts.map +0 -1
- package/src/bridge/messages/ready.ts +0 -48
- /package/dist/host/lifecycle/{ready.js → protocolVersion.js} +0 -0
- /package/src/host/lifecycle/{ready.ts → protocolVersion.ts} +0 -0
|
@@ -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,7 @@ 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
|
-
export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION =
|
|
66
|
+
export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 3
|
|
59
67
|
|
|
60
68
|
/**
|
|
61
69
|
* A single entry in the bridge message log. Kept intentionally plain so the log
|
|
@@ -75,6 +83,7 @@ export class SandboxBridge {
|
|
|
75
83
|
private listeners = new Set<() => void>()
|
|
76
84
|
private messageLog: MessageLogEntry[] = []
|
|
77
85
|
private messageLogListeners = new Set<() => void>()
|
|
86
|
+
private nextRequestId = 1
|
|
78
87
|
private readonly pendingCreatePage = new PendingRequests<CreatePageResult>(
|
|
79
88
|
"custom-block-create-page",
|
|
80
89
|
)
|
|
@@ -90,20 +99,22 @@ export class SandboxBridge {
|
|
|
90
99
|
private readonly pendingUpdatePage = new PendingRequests<UpdatePageResult>(
|
|
91
100
|
"custom-block-update-page",
|
|
92
101
|
)
|
|
93
|
-
private
|
|
102
|
+
private hasSentConnect = false
|
|
103
|
+
private hasReceivedInit = false
|
|
104
|
+
private initializationId: string | undefined
|
|
94
105
|
private latestDataSourceBindings: NotionDataSourceBindings = {}
|
|
95
|
-
private
|
|
106
|
+
private isMockState = false
|
|
107
|
+
private resolveInit: (() => void) | undefined
|
|
96
108
|
private rejectInit: ((reason: Error) => void) | undefined
|
|
97
|
-
private readonly initMessage: Promise<
|
|
109
|
+
private readonly initMessage: Promise<void> = new Promise(
|
|
98
110
|
(resolve, reject) => {
|
|
99
111
|
this.resolveInit = resolve
|
|
100
112
|
this.rejectInit = reject
|
|
101
113
|
},
|
|
102
114
|
)
|
|
103
|
-
private manifest: CustomBlockManifest | null = null
|
|
104
115
|
|
|
105
116
|
constructor() {
|
|
106
|
-
// `
|
|
117
|
+
// `connect` is sent later by `initCustomBlock` (after the manifest fetch
|
|
107
118
|
// resolves). Top-level / no-iframe rejection is handled there too, so
|
|
108
119
|
// the constructor just attaches the listener.
|
|
109
120
|
if (typeof window !== "undefined") {
|
|
@@ -136,7 +147,7 @@ export class SandboxBridge {
|
|
|
136
147
|
return () => this.messageLogListeners.delete(listener)
|
|
137
148
|
}
|
|
138
149
|
|
|
139
|
-
awaitInit(signal?: AbortSignal): Promise<
|
|
150
|
+
awaitInit(signal?: AbortSignal): Promise<void> {
|
|
140
151
|
if (!signal) {
|
|
141
152
|
return this.initMessage
|
|
142
153
|
}
|
|
@@ -148,9 +159,9 @@ export class SandboxBridge {
|
|
|
148
159
|
const onAbort = () => reject(signal.reason)
|
|
149
160
|
signal.addEventListener("abort", onAbort, { once: true })
|
|
150
161
|
this.initMessage.then(
|
|
151
|
-
|
|
162
|
+
() => {
|
|
152
163
|
signal.removeEventListener("abort", onAbort)
|
|
153
|
-
resolve(
|
|
164
|
+
resolve()
|
|
154
165
|
},
|
|
155
166
|
err => {
|
|
156
167
|
signal.removeEventListener("abort", onAbort)
|
|
@@ -160,34 +171,38 @@ export class SandboxBridge {
|
|
|
160
171
|
})
|
|
161
172
|
}
|
|
162
173
|
|
|
163
|
-
|
|
174
|
+
sendConnect(manifestResult: ManifestLoadResult) {
|
|
164
175
|
if (typeof window === "undefined") {
|
|
165
176
|
return
|
|
166
177
|
}
|
|
167
|
-
if (this.
|
|
168
|
-
console.warn("[custom-blocks-sdk] ignoring duplicate
|
|
178
|
+
if (this.hasSentConnect) {
|
|
179
|
+
console.warn("[custom-blocks-sdk] ignoring duplicate connect message")
|
|
169
180
|
return
|
|
170
181
|
}
|
|
171
182
|
const { manifest, error } = manifestResult
|
|
172
|
-
this.
|
|
173
|
-
|
|
174
|
-
|
|
183
|
+
this.hasSentConnect = true
|
|
184
|
+
const initializationId = `custom-block-initialization-${this.nextRequestId}`
|
|
185
|
+
this.nextRequestId += 1
|
|
186
|
+
this.initializationId = initializationId
|
|
187
|
+
const connectMessage: ConnectMessage =
|
|
175
188
|
error !== undefined
|
|
176
189
|
? {
|
|
177
|
-
type: "
|
|
190
|
+
type: "connect",
|
|
191
|
+
initializationId,
|
|
178
192
|
status: "error",
|
|
179
193
|
bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
|
|
180
194
|
sdkVersion: CUSTOM_BLOCKS_SDK_VERSION,
|
|
181
195
|
error,
|
|
182
196
|
}
|
|
183
197
|
: {
|
|
184
|
-
type: "
|
|
198
|
+
type: "connect",
|
|
199
|
+
initializationId,
|
|
185
200
|
status: "success",
|
|
186
201
|
bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
|
|
187
202
|
sdkVersion: CUSTOM_BLOCKS_SDK_VERSION,
|
|
188
|
-
manifest,
|
|
203
|
+
...(manifest !== null ? { manifest } : {}),
|
|
189
204
|
}
|
|
190
|
-
this.postToHost(
|
|
205
|
+
this.postToHost(connectMessage)
|
|
191
206
|
}
|
|
192
207
|
|
|
193
208
|
private postToHost(message: unknown) {
|
|
@@ -252,7 +267,18 @@ export class SandboxBridge {
|
|
|
252
267
|
// `init` is the only message valid before initialization. Handle it up
|
|
253
268
|
// front so every later case can assume `status === "initialized"`.
|
|
254
269
|
if (message.type === "init") {
|
|
255
|
-
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)
|
|
256
282
|
return
|
|
257
283
|
}
|
|
258
284
|
|
|
@@ -307,10 +333,12 @@ export class SandboxBridge {
|
|
|
307
333
|
const dataSources = reuseDataSourcesForUnchangedBindings({
|
|
308
334
|
previousDataSources: hostState.dataSources,
|
|
309
335
|
previousBindings: this.latestDataSourceBindings,
|
|
310
|
-
nextDataSources:
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
336
|
+
nextDataSources: this.isMockState
|
|
337
|
+
? resolveMockDataSources(nextBindings)
|
|
338
|
+
: resolveDataSources({
|
|
339
|
+
manifest: hostState.manifest,
|
|
340
|
+
dataSourceBindings: nextBindings,
|
|
341
|
+
}),
|
|
314
342
|
nextBindings,
|
|
315
343
|
})
|
|
316
344
|
this.latestDataSourceBindings = nextBindings
|
|
@@ -449,16 +477,6 @@ export class SandboxBridge {
|
|
|
449
477
|
return this.hostState
|
|
450
478
|
}
|
|
451
479
|
|
|
452
|
-
/**
|
|
453
|
-
* The author-declared manifest loaded from `custom_blocks.json` and forwarded
|
|
454
|
-
* to the host in `ready`. `null` when it failed to load/parse and the host
|
|
455
|
-
* should reject init via `ready.status: "error"`. Static for the lifetime
|
|
456
|
-
* of the sandbox.
|
|
457
|
-
*/
|
|
458
|
-
getManifest(): CustomBlockManifest | null {
|
|
459
|
-
return this.manifest
|
|
460
|
-
}
|
|
461
|
-
|
|
462
480
|
/**
|
|
463
481
|
* Apply an `init` payload as if it had arrived from the host. Lets callers
|
|
464
482
|
* seed the bridge directly (e.g. the React provider's standalone preview
|
|
@@ -466,10 +484,14 @@ export class SandboxBridge {
|
|
|
466
484
|
* of why it's being seeded.
|
|
467
485
|
*/
|
|
468
486
|
setMockState(message: InitMessage) {
|
|
469
|
-
this.
|
|
487
|
+
this.isMockState = true
|
|
488
|
+
this.applyInit(v.parse(initMessageSchema, message), false)
|
|
470
489
|
}
|
|
471
490
|
|
|
472
|
-
private applyInit(message: InitMessage) {
|
|
491
|
+
private applyInit(message: InitMessage, postResult: boolean) {
|
|
492
|
+
if (postResult) {
|
|
493
|
+
this.isMockState = false
|
|
494
|
+
}
|
|
473
495
|
if (message.status === "error") {
|
|
474
496
|
// The host couldn't construct block location for this block (most commonly the parent record
|
|
475
497
|
// failed to resolve). Surface the failure through the `awaitInit` promise so callers see
|
|
@@ -481,7 +503,7 @@ export class SandboxBridge {
|
|
|
481
503
|
`[custom-blocks-sdk] host reported init error (${message.error.code}): ${message.error.message}`,
|
|
482
504
|
)
|
|
483
505
|
if (this.rejectInit) {
|
|
484
|
-
this.rejectInit(new
|
|
506
|
+
this.rejectInit(new CustomBlockInitializationError(message.error))
|
|
485
507
|
this.resolveInit = undefined
|
|
486
508
|
this.rejectInit = undefined
|
|
487
509
|
}
|
|
@@ -491,10 +513,28 @@ export class SandboxBridge {
|
|
|
491
513
|
const { blockId, parent, page } = message
|
|
492
514
|
|
|
493
515
|
this.latestDataSourceBindings = message.dataSources.bindings
|
|
494
|
-
const dataSources =
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
}
|
|
498
538
|
this.hostState = {
|
|
499
539
|
status: "initialized",
|
|
500
540
|
theme: message.theme,
|
|
@@ -502,15 +542,24 @@ export class SandboxBridge {
|
|
|
502
542
|
parent,
|
|
503
543
|
page,
|
|
504
544
|
currentUser: message.currentUser,
|
|
545
|
+
manifest: message.manifest,
|
|
505
546
|
dataSources,
|
|
506
547
|
dataSourceState: {},
|
|
507
548
|
}
|
|
508
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
|
+
}
|
|
509
558
|
// Resolve the awaitInit promise once. Subsequent `init` messages
|
|
510
559
|
// (the host shouldn't send these, but be tolerant) update state but
|
|
511
560
|
// don't re-resolve.
|
|
512
561
|
if (this.resolveInit) {
|
|
513
|
-
this.resolveInit(
|
|
562
|
+
this.resolveInit()
|
|
514
563
|
this.resolveInit = undefined
|
|
515
564
|
this.rejectInit = undefined
|
|
516
565
|
}
|
|
@@ -892,6 +941,51 @@ function makeDataSourceSubscriptionId(args: { key: string }): string {
|
|
|
892
941
|
return `data-source:${encodeURIComponent(key)}`
|
|
893
942
|
}
|
|
894
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
|
+
|
|
895
989
|
function formatInvalidHostReason(
|
|
896
990
|
incomingType: string | undefined,
|
|
897
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
|
}
|
|
@@ -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
|
*/
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import { customBlockErrorInfoSchema } from "../errors.js"
|
|
3
|
+
import { manifestSchema } from "../manifest.js"
|
|
4
|
+
|
|
5
|
+
const connectMessageCommonEntries = {
|
|
6
|
+
type: v.literal("connect"),
|
|
7
|
+
initializationId: v.string(),
|
|
8
|
+
/**
|
|
9
|
+
* Used to ensure that the host and client are using the same version of the bridge protocol. A
|
|
10
|
+
* single host needs to support multiple custom blocks built with different versions of the bridge
|
|
11
|
+
* protocol. Increment this number any time a breaking change is made to the bridge protocol.
|
|
12
|
+
*/
|
|
13
|
+
bridgeProtocolVersion: v.number(),
|
|
14
|
+
/**
|
|
15
|
+
* Semver version of the SDK package that sent this connect message. Should only be used for
|
|
16
|
+
* analytics purposes. Business logic should compare against `bridgeProtocolVersion` instead.
|
|
17
|
+
*/
|
|
18
|
+
sdkVersion: v.string(),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* First message the sandbox sends after mount. It identifies the sandbox and starts the bridge
|
|
23
|
+
* initialization exchange. The host echoes `initializationId` in its `init` response.
|
|
24
|
+
*/
|
|
25
|
+
export const connectMessageSchema = v.variant("status", [
|
|
26
|
+
v.object({
|
|
27
|
+
status: v.literal("success"),
|
|
28
|
+
/**
|
|
29
|
+
* The data sources and settings the custom block expects. Hosts may omit
|
|
30
|
+
* this when they already have an authoritative manifest.
|
|
31
|
+
*/
|
|
32
|
+
manifest: v.optional(manifestSchema),
|
|
33
|
+
...connectMessageCommonEntries,
|
|
34
|
+
}),
|
|
35
|
+
v.object({
|
|
36
|
+
status: v.literal("error"),
|
|
37
|
+
error: customBlockErrorInfoSchema,
|
|
38
|
+
...connectMessageCommonEntries,
|
|
39
|
+
}),
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
export type ConnectMessage = v.InferOutput<typeof connectMessageSchema>
|
|
@@ -2,18 +2,18 @@ import * as v from "valibot"
|
|
|
2
2
|
import { notionDataSourceBindingsSchema } from "../dataSources/dataSource.js"
|
|
3
3
|
import type { CustomBlockErrorCode, CustomBlockErrorInfo } from "../errors.js"
|
|
4
4
|
import { notionBlockIdSchema } from "../ids.js"
|
|
5
|
+
import { manifestSchema } from "../manifest.js"
|
|
5
6
|
import { customBlockPageSchema } from "../pages/page.js"
|
|
6
7
|
import { notionParentSchema } from "../parent.js"
|
|
7
8
|
import { notionThemeSchema } from "../theme.js"
|
|
8
9
|
import { notionUserSchema } from "../users/user.js"
|
|
10
|
+
import type { CustomBlockInitResultErrorCode } from "./initResult.js"
|
|
9
11
|
|
|
10
12
|
// The schema accepts any string code. The type lists known codes for
|
|
11
13
|
// autocomplete, with an open string fallback for newer senders.
|
|
12
14
|
export const customBlockInitErrorCodeSchema = v.string()
|
|
13
15
|
|
|
14
16
|
export type CustomBlockInitErrorCode = CustomBlockErrorCode<
|
|
15
|
-
| "no_ready"
|
|
16
|
-
| "invalid_ready"
|
|
17
17
|
| "manifest_unavailable"
|
|
18
18
|
| "manifest_invalid"
|
|
19
19
|
| "invalid_protocol_version"
|
|
@@ -24,47 +24,60 @@ export type CustomBlockInitErrorCode = CustomBlockErrorCode<
|
|
|
24
24
|
| "data_source_unavailable"
|
|
25
25
|
| "missing_property_binding"
|
|
26
26
|
| "invalid_property_binding"
|
|
27
|
-
| "not_in_iframe"
|
|
28
|
-
| "init_timeout"
|
|
29
27
|
>
|
|
30
28
|
|
|
31
29
|
export type CustomBlockInitErrorInfo =
|
|
32
30
|
CustomBlockErrorInfo<CustomBlockInitErrorCode>
|
|
33
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Every initialization failure visible to block code. This combines errors
|
|
34
|
+
* received in `init`, errors the sandbox reports in `initResult`, and failures
|
|
35
|
+
* detected locally while running `initCustomBlock()`.
|
|
36
|
+
*/
|
|
37
|
+
export type CustomBlockInitializationErrorCode =
|
|
38
|
+
| CustomBlockInitErrorCode
|
|
39
|
+
| CustomBlockInitResultErrorCode
|
|
40
|
+
| CustomBlockErrorCode<"not_in_iframe" | "init_timeout">
|
|
41
|
+
|
|
42
|
+
export type CustomBlockInitializationErrorInfo =
|
|
43
|
+
CustomBlockErrorInfo<CustomBlockInitializationErrorCode>
|
|
44
|
+
|
|
34
45
|
export const customBlockInitErrorInfoSchema = v.object({
|
|
35
46
|
code: customBlockInitErrorCodeSchema,
|
|
36
47
|
message: v.string(),
|
|
37
48
|
isRetryable: v.boolean(),
|
|
38
49
|
})
|
|
39
50
|
|
|
40
|
-
export class
|
|
51
|
+
export class CustomBlockInitializationError
|
|
41
52
|
extends Error
|
|
42
53
|
implements CustomBlockErrorInfo
|
|
43
54
|
{
|
|
44
|
-
constructor(error:
|
|
55
|
+
constructor(error: CustomBlockInitializationErrorInfo) {
|
|
45
56
|
super(error.message)
|
|
46
|
-
this.name = "
|
|
57
|
+
this.name = "CustomBlockInitializationError"
|
|
47
58
|
this.code = error.code
|
|
48
59
|
this.isRetryable = error.isRetryable
|
|
49
60
|
}
|
|
50
61
|
|
|
51
|
-
code:
|
|
62
|
+
code: CustomBlockInitializationErrorCode
|
|
52
63
|
isRetryable: boolean
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
/**
|
|
56
67
|
* Initialization message sent by the host to the sandbox exactly once, in response to the
|
|
57
|
-
* sandbox's `
|
|
58
|
-
*
|
|
68
|
+
* sandbox's `connect` message. The sandbox echoes `initializationId` in `initResult`. After
|
|
69
|
+
* successful initialization, live updates flow through narrower messages.
|
|
59
70
|
*/
|
|
60
71
|
export const initMessageSchema = v.variant("status", [
|
|
61
72
|
v.object({
|
|
62
73
|
type: v.literal("init"),
|
|
74
|
+
initializationId: v.string(),
|
|
63
75
|
status: v.literal("success"),
|
|
64
76
|
theme: notionThemeSchema,
|
|
65
77
|
blockId: notionBlockIdSchema,
|
|
66
78
|
parent: notionParentSchema,
|
|
67
79
|
page: customBlockPageSchema,
|
|
80
|
+
manifest: manifestSchema,
|
|
68
81
|
dataSources: v.object({
|
|
69
82
|
bindings: notionDataSourceBindingsSchema,
|
|
70
83
|
}),
|
|
@@ -72,6 +85,7 @@ export const initMessageSchema = v.variant("status", [
|
|
|
72
85
|
}),
|
|
73
86
|
v.object({
|
|
74
87
|
type: v.literal("init"),
|
|
88
|
+
initializationId: v.string(),
|
|
75
89
|
status: v.literal("error"),
|
|
76
90
|
error: customBlockInitErrorInfoSchema,
|
|
77
91
|
}),
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as v from "valibot"
|
|
2
|
+
import type { CustomBlockErrorCode, CustomBlockErrorInfo } from "../errors.js"
|
|
3
|
+
|
|
4
|
+
// The schema accepts any string code. The type lists known codes for
|
|
5
|
+
// autocomplete, with an open string fallback for newer senders.
|
|
6
|
+
export const customBlockInitResultErrorCodeSchema = v.string()
|
|
7
|
+
|
|
8
|
+
export type CustomBlockInitResultErrorCode =
|
|
9
|
+
CustomBlockErrorCode<"invalid_init_bindings">
|
|
10
|
+
|
|
11
|
+
export type CustomBlockInitResultErrorInfo =
|
|
12
|
+
CustomBlockErrorInfo<CustomBlockInitResultErrorCode>
|
|
13
|
+
|
|
14
|
+
export const customBlockInitResultErrorInfoSchema = v.object({
|
|
15
|
+
code: customBlockInitResultErrorCodeSchema,
|
|
16
|
+
message: v.string(),
|
|
17
|
+
isRetryable: v.boolean(),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Acknowledges whether the sandbox successfully applied the host's `init` response.
|
|
22
|
+
*/
|
|
23
|
+
export const initResultMessageSchema = v.variant("status", [
|
|
24
|
+
v.object({
|
|
25
|
+
type: v.literal("initResult"),
|
|
26
|
+
initializationId: v.string(),
|
|
27
|
+
status: v.literal("success"),
|
|
28
|
+
}),
|
|
29
|
+
v.object({
|
|
30
|
+
type: v.literal("initResult"),
|
|
31
|
+
initializationId: v.string(),
|
|
32
|
+
status: v.literal("error"),
|
|
33
|
+
error: customBlockInitResultErrorInfoSchema,
|
|
34
|
+
}),
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
export type InitResultMessage = v.InferOutput<typeof initResultMessageSchema>
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as v from "valibot"
|
|
2
|
+
import { connectMessageSchema } from "./connect.js"
|
|
2
3
|
import { createPageMessageSchema } from "./createPage.js"
|
|
3
4
|
import { getPageMessageSchema } from "./getPage.js"
|
|
4
5
|
import { getUserMessageSchema } from "./getUser.js"
|
|
6
|
+
import { initResultMessageSchema } from "./initResult.js"
|
|
5
7
|
import { invalidHostMessageSchema } from "./invalidHostMessage.js"
|
|
6
8
|
import { listUsersMessageSchema } from "./listUsers.js"
|
|
7
9
|
import { queryDataSourceMessageSchema } from "./queryDataSource.js"
|
|
8
|
-
import { readyMessageSchema } from "./ready.js"
|
|
9
10
|
import { resizeMessageSchema } from "./resize.js"
|
|
10
11
|
import { updatePageMessageSchema } from "./updatePage.js"
|
|
11
12
|
|
|
@@ -14,7 +15,8 @@ import { updatePageMessageSchema } from "./updatePage.js"
|
|
|
14
15
|
* inbound traffic from sandboxes without re-implementing validation logic.
|
|
15
16
|
*/
|
|
16
17
|
export const sandboxToHostMessageSchema = v.union([
|
|
17
|
-
|
|
18
|
+
connectMessageSchema,
|
|
19
|
+
initResultMessageSchema,
|
|
18
20
|
queryDataSourceMessageSchema,
|
|
19
21
|
createPageMessageSchema,
|
|
20
22
|
getPageMessageSchema,
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
getDataSourceQueryView as getDataSourceQueryViewWithBridge,
|
|
18
18
|
} from "./hostState.js"
|
|
19
19
|
import type { ManifestLoadResult } from "./loadManifest.js"
|
|
20
|
-
import type { CustomBlockManifest } from "./manifest.js"
|
|
21
20
|
import type { InitMessage } from "./messages/init.js"
|
|
22
21
|
import { type MessageLogEntry, SandboxBridge } from "./SandboxBridge.js"
|
|
23
22
|
|
|
@@ -26,11 +25,11 @@ export type { MessageLogEntry }
|
|
|
26
25
|
const bridge = new SandboxBridge()
|
|
27
26
|
|
|
28
27
|
export const customBlockHost = {
|
|
29
|
-
|
|
30
|
-
bridge.
|
|
28
|
+
sendConnect: (manifestResult: ManifestLoadResult) => {
|
|
29
|
+
bridge.sendConnect(manifestResult)
|
|
31
30
|
},
|
|
32
31
|
|
|
33
|
-
awaitInit: (signal?: AbortSignal): Promise<
|
|
32
|
+
awaitInit: (signal?: AbortSignal): Promise<void> => {
|
|
34
33
|
return bridge.awaitInit(signal)
|
|
35
34
|
},
|
|
36
35
|
|
|
@@ -42,10 +41,6 @@ export const customBlockHost = {
|
|
|
42
41
|
return bridge.getHostState()
|
|
43
42
|
},
|
|
44
43
|
|
|
45
|
-
getManifest: (): CustomBlockManifest | null => {
|
|
46
|
-
return bridge.getManifest()
|
|
47
|
-
},
|
|
48
|
-
|
|
49
44
|
/**
|
|
50
45
|
* Apply an `init` payload directly, bypassing the postMessage handshake for
|
|
51
46
|
* standalone preview state.
|