@notionhq/custom-blocks 0.1.43 → 0.1.44
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 +2 -2
- package/dist/bridge/SandboxBridge.d.ts +9 -2
- package/dist/bridge/SandboxBridge.d.ts.map +1 -1
- package/dist/bridge/SandboxBridge.js +23 -8
- package/dist/bridge/dataSources/subscribe.d.ts +3 -0
- package/dist/bridge/dataSources/subscribe.d.ts.map +1 -0
- package/dist/bridge/dataSources/subscribe.js +63 -0
- package/dist/bridge/notifyListener.d.ts +3 -0
- package/dist/bridge/notifyListener.d.ts.map +1 -0
- package/dist/bridge/notifyListener.js +16 -0
- package/dist/bridge/sandboxClient.d.ts +3 -3
- package/dist/bridge/sandboxClient.d.ts.map +1 -1
- package/dist/bridge/sandboxClient.js +3 -3
- package/dist/customBlock.d.ts +6 -0
- package/dist/customBlock.d.ts.map +1 -1
- package/dist/customBlock.js +6 -0
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/index.js +1 -0
- package/dist/protocol/messages/sandboxToHost.d.ts +3 -0
- package/dist/protocol/messages/sandboxToHost.js +2 -0
- package/dist/protocol/messages/unsubscribeDataSourceQuery.d.ts +10 -0
- package/dist/protocol/messages/unsubscribeDataSourceQuery.js +9 -0
- package/dist/react/useDataSource.d.ts.map +1 -1
- package/dist/react/useDataSource.js +26 -29
- package/dist/types.d.ts +15 -15
- package/dist/types.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/docs/data-sources.md +212 -72
- package/docs/lifecycle.md +1 -11
- package/docs/pages.md +0 -2
- package/docs/users.md +0 -1
- package/package.json +1 -1
- package/src/bridge/SandboxBridge.ts +41 -13
- package/src/bridge/dataSources/subscribe.ts +97 -0
- package/src/bridge/notifyListener.ts +14 -0
- package/src/bridge/sandboxClient.ts +7 -8
- package/src/customBlock.ts +7 -0
- package/src/react/useDataSource.ts +32 -39
- package/src/types.ts +18 -15
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { SubscribeToDataSourceArgs } from "../../types.js"
|
|
2
|
+
import type {
|
|
3
|
+
CustomBlockHostState,
|
|
4
|
+
InitializedHostState,
|
|
5
|
+
} from "../hostState.js"
|
|
6
|
+
import { notifyListener } from "../notifyListener.js"
|
|
7
|
+
import { customBlockDataSources, customBlockHost } from "../sandboxClient.js"
|
|
8
|
+
|
|
9
|
+
export function subscribeToDataSource({
|
|
10
|
+
key,
|
|
11
|
+
onSnapshot,
|
|
12
|
+
options: providedOptions = {},
|
|
13
|
+
}: SubscribeToDataSourceArgs): () => void {
|
|
14
|
+
const options = structuredClone(providedOptions)
|
|
15
|
+
const subscriptionId = customBlockDataSources.createSubscriptionId()
|
|
16
|
+
let active = true
|
|
17
|
+
let lastSnapshotInputs:
|
|
18
|
+
| {
|
|
19
|
+
status: CustomBlockHostState["status"]
|
|
20
|
+
dataSource: InitializedHostState["dataSources"][number] | undefined
|
|
21
|
+
queryState: InitializedHostState["dataSourceState"][string] | undefined
|
|
22
|
+
}
|
|
23
|
+
| undefined
|
|
24
|
+
let lastMatchingSignature: string | null | undefined
|
|
25
|
+
|
|
26
|
+
const deliverSnapshotIfChanged = (
|
|
27
|
+
hostState: CustomBlockHostState,
|
|
28
|
+
dataSource: InitializedHostState["dataSources"][number] | undefined,
|
|
29
|
+
queryState: InitializedHostState["dataSourceState"][string] | undefined,
|
|
30
|
+
) => {
|
|
31
|
+
if (
|
|
32
|
+
lastSnapshotInputs !== undefined &&
|
|
33
|
+
lastSnapshotInputs.status === hostState.status &&
|
|
34
|
+
lastSnapshotInputs.dataSource === dataSource &&
|
|
35
|
+
lastSnapshotInputs.queryState === queryState
|
|
36
|
+
) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
const snapshot = customBlockDataSources.getView(
|
|
40
|
+
hostState,
|
|
41
|
+
key,
|
|
42
|
+
subscriptionId,
|
|
43
|
+
)
|
|
44
|
+
// Record inputs before calling consumer code, which can trigger another notification.
|
|
45
|
+
lastSnapshotInputs = { status: hostState.status, dataSource, queryState }
|
|
46
|
+
notifyListener(() => onSnapshot(snapshot))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handleHostChange = () => {
|
|
50
|
+
if (!active) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
const hostState = customBlockHost.getState()
|
|
54
|
+
if (hostState.status !== "initialized") {
|
|
55
|
+
deliverSnapshotIfChanged(hostState, undefined, undefined)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const dataSource = hostState.dataSources.find(source => source.key === key)
|
|
60
|
+
const signature =
|
|
61
|
+
dataSource === undefined ? null : JSON.stringify(dataSource)
|
|
62
|
+
if (signature !== lastMatchingSignature) {
|
|
63
|
+
const forceRequery = lastMatchingSignature !== undefined
|
|
64
|
+
lastMatchingSignature = signature
|
|
65
|
+
// Querying updates SDK state and calls this listener again.
|
|
66
|
+
customBlockDataSources.query({
|
|
67
|
+
subscriptionId,
|
|
68
|
+
key,
|
|
69
|
+
options,
|
|
70
|
+
forceRequery,
|
|
71
|
+
})
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
deliverSnapshotIfChanged(
|
|
75
|
+
hostState,
|
|
76
|
+
dataSource,
|
|
77
|
+
hostState.dataSourceState[subscriptionId],
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const unsubscribeHost = customBlockHost.subscribe(handleHostChange)
|
|
82
|
+
try {
|
|
83
|
+
handleHostChange()
|
|
84
|
+
} catch (error) {
|
|
85
|
+
unsubscribeHost()
|
|
86
|
+
customBlockDataSources.release(subscriptionId)
|
|
87
|
+
throw error
|
|
88
|
+
}
|
|
89
|
+
return () => {
|
|
90
|
+
if (!active) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
active = false
|
|
94
|
+
unsubscribeHost()
|
|
95
|
+
customBlockDataSources.release(subscriptionId)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Report consumer errors without interrupting bridge operations or other listeners. */
|
|
2
|
+
export function notifyListener(listener: () => void): void {
|
|
3
|
+
try {
|
|
4
|
+
listener()
|
|
5
|
+
} catch (error) {
|
|
6
|
+
if (typeof globalThis.reportError === "function") {
|
|
7
|
+
globalThis.reportError(error)
|
|
8
|
+
} else {
|
|
9
|
+
setTimeout(() => {
|
|
10
|
+
throw error
|
|
11
|
+
}, 0)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -10,7 +10,6 @@ import type {
|
|
|
10
10
|
NotionUserId,
|
|
11
11
|
UpdatePageArgs,
|
|
12
12
|
UpdatePageResult,
|
|
13
|
-
UseDataSourceOptions,
|
|
14
13
|
} from "../types.js"
|
|
15
14
|
import {
|
|
16
15
|
type CustomBlockHostState,
|
|
@@ -18,7 +17,11 @@ import {
|
|
|
18
17
|
getDataSourceQueryView as getDataSourceQueryViewWithBridge,
|
|
19
18
|
} from "./hostState.js"
|
|
20
19
|
import type { ManifestLoadResult } from "./loadManifest.js"
|
|
21
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
type MessageLogEntry,
|
|
22
|
+
type QueryDataSourceArgs,
|
|
23
|
+
SandboxBridge,
|
|
24
|
+
} from "./SandboxBridge.js"
|
|
22
25
|
|
|
23
26
|
let bridge: SandboxBridge | undefined
|
|
24
27
|
let didWarnAboutPagesDelete = false
|
|
@@ -73,12 +76,8 @@ export const customBlockDataSources = {
|
|
|
73
76
|
return getBridge().createDataSourceSubscriptionId()
|
|
74
77
|
},
|
|
75
78
|
|
|
76
|
-
query: (
|
|
77
|
-
|
|
78
|
-
key: string,
|
|
79
|
-
options?: UseDataSourceOptions,
|
|
80
|
-
) => {
|
|
81
|
-
getBridge().queryDataSource(subscriptionId, key, options)
|
|
79
|
+
query: (args: QueryDataSourceArgs) => {
|
|
80
|
+
getBridge().queryDataSource(args)
|
|
82
81
|
},
|
|
83
82
|
|
|
84
83
|
release: (subscriptionId: string) => {
|
package/src/customBlock.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { NotionParent } from "@notionhq/custom-blocks-protocol/parent.js"
|
|
|
7
7
|
import type { NotionTheme } from "@notionhq/custom-blocks-protocol/theme.js"
|
|
8
8
|
import type { NotionUser } from "@notionhq/custom-blocks-protocol/users/user.js"
|
|
9
9
|
import { autoResize } from "./autoResize.js"
|
|
10
|
+
import { subscribeToDataSource } from "./bridge/dataSources/subscribe.js"
|
|
10
11
|
import type {
|
|
11
12
|
CustomBlockHostState,
|
|
12
13
|
InitializedHostState,
|
|
@@ -72,6 +73,12 @@ export const customBlock = {
|
|
|
72
73
|
},
|
|
73
74
|
|
|
74
75
|
autoResize,
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Registers a listener that receives the current query snapshot and later updates.
|
|
79
|
+
* Returns a function that removes this listener. Other listeners remain subscribed.
|
|
80
|
+
*/
|
|
81
|
+
subscribeToDataSource,
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
let lastHostState: CustomBlockHostState | undefined
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { useEffect, useState } from "react"
|
|
2
2
|
import { getDataSourceQueryOptionsIdentity } from "../bridge/dataSources/query.js"
|
|
3
|
-
import {
|
|
3
|
+
import { customBlock } from "../customBlock.js"
|
|
4
4
|
import type { UseDataSourceOptions, UseDataSourceResult } from "../types.js"
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
const EMPTY_DATA_SOURCE_RESULT: UseDataSourceResult = {
|
|
7
|
+
items: [],
|
|
8
|
+
propertySchemasById: {},
|
|
9
|
+
propertyIdsByKey: {},
|
|
10
|
+
propertySchemasByKey: {},
|
|
11
|
+
isLoading: false,
|
|
12
|
+
hasMore: false,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type DataSourceHookState = {
|
|
16
|
+
key: string
|
|
17
|
+
optionsIdentity: string
|
|
18
|
+
snapshot: UseDataSourceResult
|
|
19
|
+
}
|
|
6
20
|
|
|
7
21
|
/**
|
|
8
22
|
* Reads from the data source mapped to the given semantic `key`.
|
|
@@ -18,45 +32,24 @@ export function useDataSource(
|
|
|
18
32
|
key: string,
|
|
19
33
|
options?: UseDataSourceOptions,
|
|
20
34
|
): UseDataSourceResult {
|
|
21
|
-
const host = useCustomBlockHost()
|
|
22
|
-
const [subscriptionId] = useState(() =>
|
|
23
|
-
customBlockDataSources.createSubscriptionId(),
|
|
24
|
-
)
|
|
25
35
|
const optionsIdentity = getDataSourceQueryOptionsIdentity(options)
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const isInitialized = host.status === "initialized"
|
|
36
|
+
const [state, setState] = useState<DataSourceHookState>(() => ({
|
|
37
|
+
key,
|
|
38
|
+
optionsIdentity,
|
|
39
|
+
snapshot: EMPTY_DATA_SOURCE_RESULT,
|
|
40
|
+
}))
|
|
32
41
|
// biome-ignore lint/correctness/useExhaustiveDependencies(options): Compare options by value through optionsIdentity. Object identity alone must not trigger queries.
|
|
33
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies(optionsIdentity): Changes to the serialized options must trigger a query even though the callback reads options.
|
|
34
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies(matchingDataSource): Requery when the matching data source changes, even when the key and options are unchanged.
|
|
35
42
|
useEffect(() => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
},
|
|
47
|
-
[subscriptionId],
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
const view = customBlockDataSources.getView(host, key, subscriptionId)
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
items: view.items,
|
|
54
|
-
collectionSchema: view.collectionSchema,
|
|
55
|
-
propertySchemasById: view.propertySchemasById,
|
|
56
|
-
propertyIdsByKey: view.propertyIdsByKey,
|
|
57
|
-
propertySchemasByKey: view.propertySchemasByKey,
|
|
58
|
-
isLoading: view.isLoading,
|
|
59
|
-
hasMore: view.hasMore,
|
|
60
|
-
error: view.error,
|
|
43
|
+
return customBlock.subscribeToDataSource({
|
|
44
|
+
key,
|
|
45
|
+
onSnapshot: snapshot => {
|
|
46
|
+
setState({ key, optionsIdentity, snapshot })
|
|
47
|
+
},
|
|
48
|
+
options,
|
|
49
|
+
})
|
|
50
|
+
}, [key, optionsIdentity])
|
|
51
|
+
if (state.key !== key || state.optionsIdentity !== optionsIdentity) {
|
|
52
|
+
return EMPTY_DATA_SOURCE_RESULT
|
|
61
53
|
}
|
|
54
|
+
return state.snapshot
|
|
62
55
|
}
|
package/src/types.ts
CHANGED
|
@@ -80,7 +80,7 @@ export type NotionPagePropertyInputMap = {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
* Consumer-facing page shape returned from
|
|
83
|
+
* Consumer-facing page shape returned from a data source snapshot. Derived from the bridge payload
|
|
84
84
|
* plus the data source's `propertyIdsByKey`.
|
|
85
85
|
*/
|
|
86
86
|
export type NotionDataSourcePage = {
|
|
@@ -176,14 +176,14 @@ export type NotionDataSourceFilter =
|
|
|
176
176
|
| { and: NotionDataSourcePropertyFilter[] }
|
|
177
177
|
|
|
178
178
|
/**
|
|
179
|
-
*
|
|
179
|
+
* Latest live snapshot of a data source query.
|
|
180
180
|
*
|
|
181
181
|
* - `items` — the rows the host has returned so far. Empty until the first response arrives.
|
|
182
182
|
* - `isLoading` — `true` while a query is in flight.
|
|
183
|
-
* - `hasMore` — `true` if the host indicated more rows are available beyond the
|
|
183
|
+
* - `hasMore` — `true` if the host indicated more rows are available beyond the requested prefix.
|
|
184
184
|
* - `error` — structured error information if the most recent query failed.
|
|
185
185
|
*/
|
|
186
|
-
export type
|
|
186
|
+
export type DataSourceSnapshot = {
|
|
187
187
|
items: NotionDataSourcePage[]
|
|
188
188
|
/**
|
|
189
189
|
* Collection/data source schema for the bound Notion data source, including raw property
|
|
@@ -211,22 +211,25 @@ export type UseDataSourceResult = {
|
|
|
211
211
|
error?: CustomBlockQueryDataSourceErrorInfo
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
export type
|
|
215
|
-
/**
|
|
216
|
-
* Maximum number of rows to request from the host. Defaults to 20.
|
|
217
|
-
*/
|
|
214
|
+
export type DataSourceQueryOptions = {
|
|
215
|
+
/** Maximum number of rows to request from the host. Defaults to 20. */
|
|
218
216
|
limit?: number
|
|
219
|
-
/**
|
|
220
|
-
* Optional property filter. The SDK resolves semantic property keys before
|
|
221
|
-
* it sends the query to the host.
|
|
222
|
-
*/
|
|
217
|
+
/** Optional property filter. The SDK resolves semantic keys before it sends the query. */
|
|
223
218
|
filter?: NotionDataSourceFilter
|
|
224
|
-
/**
|
|
225
|
-
* Optional property sorts. The host applies them in array order.
|
|
226
|
-
*/
|
|
219
|
+
/** Optional property sorts. The host applies them in array order. */
|
|
227
220
|
sorts?: NotionDataSourceSort[]
|
|
228
221
|
}
|
|
229
222
|
|
|
223
|
+
export type SubscribeToDataSourceArgs = {
|
|
224
|
+
key: string
|
|
225
|
+
onSnapshot: (snapshot: DataSourceSnapshot) => void
|
|
226
|
+
options?: DataSourceQueryOptions
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export type UseDataSourceResult = DataSourceSnapshot
|
|
230
|
+
|
|
231
|
+
export type UseDataSourceOptions = DataSourceQueryOptions
|
|
232
|
+
|
|
230
233
|
/**
|
|
231
234
|
* Parent reference accepted by `sdk.pages.create`. Mirrors Notion's public `POST /v1/pages`
|
|
232
235
|
* parent shape; see https://developers.notion.com/reference/data-source.
|