@notionhq/custom-blocks-host 0.2.21 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -239
- package/dist/browser.js +1 -1
- package/dist/createCustomBlockHost.d.ts +5 -5
- package/dist/createCustomBlockHost.d.ts.map +1 -1
- package/dist/createCustomBlockHost.js +54 -15
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lifecycle/loadCustomBlockManifest.d.ts +16 -0
- package/dist/lifecycle/loadCustomBlockManifest.d.ts.map +1 -0
- package/dist/lifecycle/loadCustomBlockManifest.js +65 -0
- package/dist/lifecycle/manifestOptions.d.ts +17 -0
- package/dist/lifecycle/manifestOptions.d.ts.map +1 -0
- package/dist/lifecycle/manifestOptions.js +1 -0
- package/dist/lifecycle/types.d.ts +14 -2
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js +1 -1
- package/package.json +1 -1
- package/src/createCustomBlockHost.ts +64 -23
- package/src/index.ts +4 -1
- package/src/lifecycle/loadCustomBlockManifest.ts +89 -0
- package/src/lifecycle/manifestOptions.ts +19 -0
- package/src/lifecycle/types.ts +17 -5
|
@@ -4,7 +4,6 @@ import { notionDataSourceBindingsSchema } from "@notionhq/custom-blocks-protocol
|
|
|
4
4
|
import type { NotionDataSourceId } from "@notionhq/custom-blocks-protocol/ids.js"
|
|
5
5
|
import { notionDataSourceIdSchema } from "@notionhq/custom-blocks-protocol/ids.js"
|
|
6
6
|
import type { CustomBlockManifest } from "@notionhq/custom-blocks-protocol/manifest.js"
|
|
7
|
-
import { manifestSchema } from "@notionhq/custom-blocks-protocol/manifest.js"
|
|
8
7
|
import {
|
|
9
8
|
type HostToSandboxMessage,
|
|
10
9
|
hostToSandboxMessageSchema,
|
|
@@ -32,15 +31,18 @@ import {
|
|
|
32
31
|
invalidPropertyBindingError,
|
|
33
32
|
missingDataSourceBindingError,
|
|
34
33
|
missingPropertyBindingError,
|
|
34
|
+
unknownInitializationError,
|
|
35
35
|
} from "./lifecycle/initErrors.js"
|
|
36
36
|
import { createInitializationController } from "./lifecycle/initializationController.js"
|
|
37
|
+
import { loadCustomBlockManifest } from "./lifecycle/loadCustomBlockManifest.js"
|
|
38
|
+
import type { CustomBlockHostManifestOptions } from "./lifecycle/manifestOptions.js"
|
|
37
39
|
import { normalizeHostToSandboxMessage } from "./lifecycle/normalizeHostToSandboxMessage.js"
|
|
38
40
|
import type {
|
|
39
41
|
CustomBlockHostDataSourcesPayload,
|
|
40
|
-
CustomBlockHostGetInitialState,
|
|
41
42
|
CustomBlockHostInitialState,
|
|
42
43
|
CustomBlockHostInitialStateResult,
|
|
43
44
|
CustomBlockHostNormalizedDataSourcesPayload,
|
|
45
|
+
CustomBlockHostPrepareInitialState,
|
|
44
46
|
} from "./lifecycle/types.js"
|
|
45
47
|
import { customBlockHostInitialStateSchema } from "./lifecycle/types.js"
|
|
46
48
|
import { createSandboxMessageRouter } from "./messages/sandboxMessageRouter.js"
|
|
@@ -63,7 +65,7 @@ import { unreachable } from "./utils.js"
|
|
|
63
65
|
// Earlier SDKs cannot represent this context, so the host reports context_unavailable.
|
|
64
66
|
const BRIDGE_PROTOCOL_VERSION_INTRODUCING_UNAVAILABLE_PAGE = 5
|
|
65
67
|
|
|
66
|
-
export type CustomBlockHostOptions = {
|
|
68
|
+
export type CustomBlockHostOptions = CustomBlockHostManifestOptions & {
|
|
67
69
|
iframe: HTMLIFrameElement
|
|
68
70
|
sandboxOrigin: string
|
|
69
71
|
/**
|
|
@@ -75,8 +77,6 @@ export type CustomBlockHostOptions = {
|
|
|
75
77
|
noConnectTimeoutMs?: number
|
|
76
78
|
noInitResultTimeoutMs?: number
|
|
77
79
|
handlers: CustomBlockHostHandlers
|
|
78
|
-
/** Called as soon as the `manifest` is available after `connect`, before `onInitialized` and binding validation. */
|
|
79
|
-
onManifest?: (manifest: CustomBlockManifest) => void
|
|
80
80
|
/** Callback fired when the initialization handshake with the sandbox completed successfully. */
|
|
81
81
|
onInitialized?: (state: {
|
|
82
82
|
manifest: CustomBlockManifest
|
|
@@ -92,7 +92,8 @@ export type CustomBlockHostOptions = {
|
|
|
92
92
|
/** Releases adapter resources when a query ends, including replacement and terminal errors. */
|
|
93
93
|
onQueryUnsubscribed: ((subscriptionId: string) => void) | undefined
|
|
94
94
|
onLog?: (direction: CustomBlockHostLogDirection, payload: unknown) => void
|
|
95
|
-
|
|
95
|
+
/** Runs once, as soon as the manifest is validated, independently of connect. */
|
|
96
|
+
prepareInitialState: CustomBlockHostPrepareInitialState
|
|
96
97
|
initialStateTimeoutMs?: number
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -122,9 +123,8 @@ export function createCustomBlockHost(
|
|
|
122
123
|
): CustomBlockHostHandle {
|
|
123
124
|
const {
|
|
124
125
|
iframe,
|
|
125
|
-
|
|
126
|
+
prepareInitialState: prepareAdapterState,
|
|
126
127
|
handlers,
|
|
127
|
-
onManifest,
|
|
128
128
|
onInitialized,
|
|
129
129
|
onConnectionFailure,
|
|
130
130
|
onLog,
|
|
@@ -148,6 +148,7 @@ export function createCustomBlockHost(
|
|
|
148
148
|
>
|
|
149
149
|
let pendingState: HostStateUpdates = {}
|
|
150
150
|
let hasPreparedInitialState = false
|
|
151
|
+
const preparationController = new AbortController()
|
|
151
152
|
let manifest: CustomBlockManifest | undefined
|
|
152
153
|
let activeDataSources: CustomBlockHostNormalizedDataSourcesPayload | undefined
|
|
153
154
|
let sandboxBridgeProtocolVersion = LATEST_BRIDGE_PROTOCOL_VERSION
|
|
@@ -343,11 +344,11 @@ export function createCustomBlockHost(
|
|
|
343
344
|
const initializationController = createInitializationController({
|
|
344
345
|
async prepareInit(connect) {
|
|
345
346
|
sandboxBridgeProtocolVersion = connect.bridgeProtocolVersion
|
|
346
|
-
const result = await
|
|
347
|
+
const result = await preparation
|
|
347
348
|
if (initializationController.status !== "preparingInit") {
|
|
348
349
|
return result
|
|
349
350
|
}
|
|
350
|
-
return
|
|
351
|
+
return finalizeInitialState(result)
|
|
351
352
|
},
|
|
352
353
|
sendInitMessage,
|
|
353
354
|
warn: payload => emit("warn", payload),
|
|
@@ -362,29 +363,25 @@ export function createCustomBlockHost(
|
|
|
362
363
|
onInitialized?.({ manifest, initialHeight: message.initialHeight })
|
|
363
364
|
}
|
|
364
365
|
},
|
|
365
|
-
onConnectionFailure
|
|
366
|
+
onConnectionFailure: failure => {
|
|
367
|
+
preparationController.abort()
|
|
368
|
+
onConnectionFailure?.(failure)
|
|
369
|
+
},
|
|
366
370
|
})
|
|
367
371
|
|
|
368
|
-
function
|
|
372
|
+
function finalizeInitialState(
|
|
369
373
|
result: CustomBlockHostInitialStateResult,
|
|
370
374
|
): CustomBlockHostInitialStateResult {
|
|
371
375
|
if (result.status === "error") {
|
|
372
376
|
return result
|
|
373
377
|
}
|
|
374
|
-
|
|
375
|
-
onManifest?.(manifest)
|
|
376
|
-
const initialStateInput = {
|
|
378
|
+
const initialState = v.parse(customBlockHostInitialStateSchema, {
|
|
377
379
|
...result.initialState,
|
|
378
380
|
...pendingState,
|
|
379
|
-
}
|
|
381
|
+
})
|
|
382
|
+
manifest = initialState.manifest
|
|
380
383
|
pendingState = {}
|
|
381
|
-
|
|
382
|
-
...v.parse(
|
|
383
|
-
v.omit(customBlockHostInitialStateSchema, ["manifest"]),
|
|
384
|
-
initialStateInput,
|
|
385
|
-
),
|
|
386
|
-
manifest,
|
|
387
|
-
}
|
|
384
|
+
|
|
388
385
|
if (
|
|
389
386
|
sandboxBridgeProtocolVersion <
|
|
390
387
|
BRIDGE_PROTOCOL_VERSION_INTRODUCING_UNAVAILABLE_PAGE &&
|
|
@@ -410,6 +407,49 @@ export function createCustomBlockHost(
|
|
|
410
407
|
return { status: "success", initialState }
|
|
411
408
|
}
|
|
412
409
|
|
|
410
|
+
// Attach rejection handling immediately: preparation may finish before connect.
|
|
411
|
+
const preparation = prepare().catch(
|
|
412
|
+
(error: unknown): CustomBlockHostInitialStateResult => {
|
|
413
|
+
if (!preparationController.signal.aborted) {
|
|
414
|
+
emit("warn", {
|
|
415
|
+
message: "Failed to prepare custom block initialization",
|
|
416
|
+
error,
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
return { status: "error", error: unknownInitializationError() }
|
|
420
|
+
},
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
async function prepare(): Promise<CustomBlockHostInitialStateResult> {
|
|
424
|
+
const loaded = await loadCustomBlockManifest({
|
|
425
|
+
...options,
|
|
426
|
+
sandboxOrigin,
|
|
427
|
+
signal: preparationController.signal,
|
|
428
|
+
onError: error => {
|
|
429
|
+
emit("warn", {
|
|
430
|
+
message: "Failed to load custom block manifest",
|
|
431
|
+
error,
|
|
432
|
+
})
|
|
433
|
+
},
|
|
434
|
+
})
|
|
435
|
+
if (loaded.status === "error") {
|
|
436
|
+
return loaded
|
|
437
|
+
}
|
|
438
|
+
if (preparationController.signal.aborted) {
|
|
439
|
+
return { status: "error", error: unknownInitializationError() }
|
|
440
|
+
}
|
|
441
|
+
const result = await prepareAdapterState({
|
|
442
|
+
manifest: loaded.manifest,
|
|
443
|
+
signal: preparationController.signal,
|
|
444
|
+
})
|
|
445
|
+
return result.status === "error"
|
|
446
|
+
? result
|
|
447
|
+
: {
|
|
448
|
+
status: "success",
|
|
449
|
+
initialState: { ...result.initialState, manifest: loaded.manifest },
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
413
453
|
const onMessage = createSandboxMessageRouter({
|
|
414
454
|
iframe,
|
|
415
455
|
sandboxOrigin,
|
|
@@ -436,6 +476,7 @@ export function createCustomBlockHost(
|
|
|
436
476
|
iframe.addEventListener("load", initializationController.onIframeLoad)
|
|
437
477
|
|
|
438
478
|
function stop() {
|
|
479
|
+
preparationController.abort()
|
|
439
480
|
window.removeEventListener("message", onMessage)
|
|
440
481
|
iframe.removeEventListener("load", initializationController.onIframeLoad)
|
|
441
482
|
initializationController.stop()
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type {
|
|
|
5
5
|
export type { NotionPropertySchemasById } from "@notionhq/custom-blocks-protocol/dataSources/propertySchema.js"
|
|
6
6
|
export type { CustomBlockErrorInfo } from "@notionhq/custom-blocks-protocol/errors.js"
|
|
7
7
|
export type { NotionPageId } from "@notionhq/custom-blocks-protocol/ids.js"
|
|
8
|
+
export type { CustomBlockManifest } from "@notionhq/custom-blocks-protocol/manifest.js"
|
|
8
9
|
export type { CreatePageMessage } from "@notionhq/custom-blocks-protocol/messages/createPage.js"
|
|
9
10
|
export type { GetPageMessage } from "@notionhq/custom-blocks-protocol/messages/getPage.js"
|
|
10
11
|
export type { GetUserMessage } from "@notionhq/custom-blocks-protocol/messages/getUser.js"
|
|
@@ -53,12 +54,14 @@ export {
|
|
|
53
54
|
createCustomBlockHost,
|
|
54
55
|
} from "./createCustomBlockHost.js"
|
|
55
56
|
export { toNotionPageId } from "./ids.js"
|
|
57
|
+
export type { CustomBlockHostManifestOptions } from "./lifecycle/manifestOptions.js"
|
|
56
58
|
export type {
|
|
57
59
|
ConnectSuccessMessage,
|
|
58
60
|
CustomBlockHostDataSourcesPayload,
|
|
59
|
-
CustomBlockHostGetInitialState,
|
|
60
61
|
CustomBlockHostInitialState,
|
|
61
62
|
CustomBlockHostInitialStateResult,
|
|
63
|
+
CustomBlockHostPreparedStateResult,
|
|
64
|
+
CustomBlockHostPrepareInitialState,
|
|
62
65
|
} from "./lifecycle/types.js"
|
|
63
66
|
export { isLocalCustomBlockUrl } from "./localBlockUrl.js"
|
|
64
67
|
export type {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CustomBlockManifest,
|
|
3
|
+
manifestSchema,
|
|
4
|
+
} from "@notionhq/custom-blocks-protocol/manifest.js"
|
|
5
|
+
import type { CustomBlockInitErrorInfo } from "@notionhq/custom-blocks-protocol/messages/init.js"
|
|
6
|
+
import * as v from "valibot"
|
|
7
|
+
import { isLocalCustomBlockUrl } from "../localBlockUrl.js"
|
|
8
|
+
import type { CustomBlockHostManifestOptions } from "./manifestOptions.js"
|
|
9
|
+
|
|
10
|
+
/** Internal to the host: preview requests never carry credentials or follow redirects. */
|
|
11
|
+
export async function loadCustomBlockManifest(
|
|
12
|
+
args: CustomBlockHostManifestOptions & {
|
|
13
|
+
sandboxOrigin: string
|
|
14
|
+
signal: AbortSignal
|
|
15
|
+
onError: (error: unknown) => void
|
|
16
|
+
},
|
|
17
|
+
): Promise<
|
|
18
|
+
| { status: "success"; manifest: CustomBlockManifest }
|
|
19
|
+
| { status: "error"; error: CustomBlockInitErrorInfo }
|
|
20
|
+
> {
|
|
21
|
+
const controller = new AbortController()
|
|
22
|
+
const abort = () => controller.abort()
|
|
23
|
+
args.signal.addEventListener("abort", abort, { once: true })
|
|
24
|
+
const cancelled = new Promise<never>((_resolve, reject) => {
|
|
25
|
+
controller.signal.addEventListener(
|
|
26
|
+
"abort",
|
|
27
|
+
() => reject(controller.signal.reason),
|
|
28
|
+
{ once: true },
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
if (args.signal.aborted) {
|
|
32
|
+
controller.abort()
|
|
33
|
+
}
|
|
34
|
+
const timeout =
|
|
35
|
+
args.manifestSource === "local"
|
|
36
|
+
? window.setTimeout(() => {
|
|
37
|
+
controller.abort(
|
|
38
|
+
new Error("Manifest loading timed out after 5000 ms."),
|
|
39
|
+
)
|
|
40
|
+
}, 5_000)
|
|
41
|
+
: undefined
|
|
42
|
+
try {
|
|
43
|
+
const input = await Promise.race([
|
|
44
|
+
args.manifestSource === "local"
|
|
45
|
+
? fetchLocalCustomBlockManifest(args.sandboxOrigin, controller.signal)
|
|
46
|
+
: args.getPersistedManifest({ signal: controller.signal }),
|
|
47
|
+
cancelled,
|
|
48
|
+
])
|
|
49
|
+
if (controller.signal.aborted) {
|
|
50
|
+
throw new Error("Manifest loading was cancelled.")
|
|
51
|
+
}
|
|
52
|
+
return { status: "success", manifest: v.parse(manifestSchema, input) }
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (!args.signal.aborted) {
|
|
55
|
+
args.onError(error)
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
status: "error",
|
|
59
|
+
error: {
|
|
60
|
+
code: "manifest_unavailable",
|
|
61
|
+
message: "The custom block manifest is unavailable.",
|
|
62
|
+
isRetryable: true,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
} finally {
|
|
66
|
+
window.clearTimeout(timeout)
|
|
67
|
+
args.signal.removeEventListener("abort", abort)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function fetchLocalCustomBlockManifest(
|
|
72
|
+
origin: string,
|
|
73
|
+
signal: AbortSignal,
|
|
74
|
+
): Promise<unknown> {
|
|
75
|
+
const url = new URL(origin)
|
|
76
|
+
if (!isLocalCustomBlockUrl(url) || url.origin === window.location.origin) {
|
|
77
|
+
throw new Error("Preview requires a separate loopback origin.")
|
|
78
|
+
}
|
|
79
|
+
const response = await fetch(new URL("/manifest", url), {
|
|
80
|
+
credentials: "omit",
|
|
81
|
+
redirect: "error",
|
|
82
|
+
cache: "no-store",
|
|
83
|
+
signal,
|
|
84
|
+
})
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new Error(`Manifest request failed: HTTP ${response.status}.`)
|
|
87
|
+
}
|
|
88
|
+
return response.json()
|
|
89
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { manifestSchema } from "@notionhq/custom-blocks-protocol/manifest.js"
|
|
2
|
+
import type { InferInput } from "valibot"
|
|
3
|
+
|
|
4
|
+
/** Selects the manifest source used during host initialization. */
|
|
5
|
+
export type CustomBlockHostManifestOptions =
|
|
6
|
+
| {
|
|
7
|
+
/** Calls getPersistedManifest for a saved definition or worker registry manifest. */
|
|
8
|
+
manifestSource: "persisted"
|
|
9
|
+
getPersistedManifest: (args: {
|
|
10
|
+
signal: AbortSignal
|
|
11
|
+
}) => Promise<InferInput<typeof manifestSchema> | undefined>
|
|
12
|
+
}
|
|
13
|
+
| {
|
|
14
|
+
/**
|
|
15
|
+
* Fetches /manifest from the loopback sandboxOrigin. No persisted fallback.
|
|
16
|
+
* Local refers to the manifest server, not the host's environment.
|
|
17
|
+
*/
|
|
18
|
+
manifestSource: "local"
|
|
19
|
+
}
|
package/src/lifecycle/types.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { notionContrastModeSchema } from "@notionhq/custom-blocks-protocol/contrast.js"
|
|
2
2
|
import { notionDataSourceBindingsSchema } from "@notionhq/custom-blocks-protocol/dataSources/dataSource.js"
|
|
3
3
|
import { notionBlockIdSchema } from "@notionhq/custom-blocks-protocol/ids.js"
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type CustomBlockManifest,
|
|
6
|
+
manifestSchema,
|
|
7
|
+
} from "@notionhq/custom-blocks-protocol/manifest.js"
|
|
5
8
|
import type { ConnectMessage } from "@notionhq/custom-blocks-protocol/messages/connect.js"
|
|
6
9
|
import type { CustomBlockInitErrorInfo } from "@notionhq/custom-blocks-protocol/messages/init.js"
|
|
7
10
|
import { customBlockPageSchema } from "@notionhq/custom-blocks-protocol/pages/page.js"
|
|
@@ -19,7 +22,7 @@ export type ConnectSuccessMessage = Extract<
|
|
|
19
22
|
export type InitStatus =
|
|
20
23
|
/** The host is waiting for the sandbox's first `connect` message. */
|
|
21
24
|
| "waitingForConnect"
|
|
22
|
-
/** The host is waiting for
|
|
25
|
+
/** The host is waiting for host state preparation to complete `init`. */
|
|
23
26
|
| "preparingInit"
|
|
24
27
|
/** The host is waiting for a matching `initResult` after it sends `init`. */
|
|
25
28
|
| "waitingForInitResult"
|
|
@@ -67,6 +70,15 @@ export type CustomBlockHostInitialStateResult =
|
|
|
67
70
|
error: CustomBlockInitErrorInfo
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
/** Adapter-owned state; the runtime adds its selected manifest to init. */
|
|
74
|
+
export type CustomBlockHostPreparedStateResult =
|
|
75
|
+
| {
|
|
76
|
+
status: "success"
|
|
77
|
+
initialState: Omit<CustomBlockHostInitialState, "manifest">
|
|
78
|
+
}
|
|
79
|
+
| { status: "error"; error: CustomBlockInitErrorInfo }
|
|
80
|
+
|
|
81
|
+
export type CustomBlockHostPrepareInitialState = (args: {
|
|
82
|
+
manifest: CustomBlockManifest
|
|
83
|
+
signal: AbortSignal
|
|
84
|
+
}) => Promise<CustomBlockHostPreparedStateResult>
|