@notionhq/custom-blocks 0.0.58

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.
Files changed (298) hide show
  1. package/HOST.md +171 -0
  2. package/README.md +57 -0
  3. package/bin/cli/attach.js +180 -0
  4. package/bin/cli/cli.js +248 -0
  5. package/bin/cli/create.js +106 -0
  6. package/bin/cli/datasources.js +255 -0
  7. package/bin/cli/deploy.js +109 -0
  8. package/bin/cli/ids.js +13 -0
  9. package/bin/cli/ntn.js +95 -0
  10. package/bin/cli/pullData.js +76 -0
  11. package/bin/cli/pullManifest.js +158 -0
  12. package/bin/cli/target.js +95 -0
  13. package/bin/src/bridge/dataSources/propertySchema.js +148 -0
  14. package/bin/src/bridge/manifest.js +40 -0
  15. package/dist/autoResize.d.ts +4 -0
  16. package/dist/autoResize.d.ts.map +1 -0
  17. package/dist/autoResize.js +71 -0
  18. package/dist/bridge/SandboxBridge.d.ts +92 -0
  19. package/dist/bridge/SandboxBridge.d.ts.map +1 -0
  20. package/dist/bridge/SandboxBridge.js +718 -0
  21. package/dist/bridge/dataSources/dataSource.d.ts +732 -0
  22. package/dist/bridge/dataSources/dataSource.d.ts.map +1 -0
  23. package/dist/bridge/dataSources/dataSource.js +37 -0
  24. package/dist/bridge/dataSources/dataSourcePage.d.ts +118 -0
  25. package/dist/bridge/dataSources/dataSourcePage.d.ts.map +1 -0
  26. package/dist/bridge/dataSources/dataSourcePage.js +14 -0
  27. package/dist/bridge/dataSources/dataSourceValue.d.ts +67 -0
  28. package/dist/bridge/dataSources/dataSourceValue.d.ts.map +1 -0
  29. package/dist/bridge/dataSources/dataSourceValue.js +14 -0
  30. package/dist/bridge/dataSources/dateValue.d.ts +158 -0
  31. package/dist/bridge/dataSources/dateValue.d.ts.map +1 -0
  32. package/dist/bridge/dataSources/dateValue.js +59 -0
  33. package/dist/bridge/dataSources/errors.d.ts +6 -0
  34. package/dist/bridge/dataSources/errors.d.ts.map +1 -0
  35. package/dist/bridge/dataSources/errors.js +4 -0
  36. package/dist/bridge/dataSources/propertySchema.d.ts +191 -0
  37. package/dist/bridge/dataSources/propertySchema.d.ts.map +1 -0
  38. package/dist/bridge/dataSources/propertySchema.js +148 -0
  39. package/dist/bridge/dataSources/recordPointer.d.ts +10 -0
  40. package/dist/bridge/dataSources/recordPointer.d.ts.map +1 -0
  41. package/dist/bridge/dataSources/recordPointer.js +8 -0
  42. package/dist/bridge/dataSources/resolve.d.ts +22 -0
  43. package/dist/bridge/dataSources/resolve.d.ts.map +1 -0
  44. package/dist/bridge/dataSources/resolve.js +65 -0
  45. package/dist/bridge/dataSources/resolveProperty.d.ts +41 -0
  46. package/dist/bridge/dataSources/resolveProperty.d.ts.map +1 -0
  47. package/dist/bridge/dataSources/resolveProperty.js +73 -0
  48. package/dist/bridge/errors.d.ts +12 -0
  49. package/dist/bridge/errors.d.ts.map +1 -0
  50. package/dist/bridge/errors.js +6 -0
  51. package/dist/bridge/hostState.d.ts +65 -0
  52. package/dist/bridge/hostState.d.ts.map +1 -0
  53. package/dist/bridge/hostState.js +65 -0
  54. package/dist/bridge/ids.d.ts +39 -0
  55. package/dist/bridge/ids.d.ts.map +1 -0
  56. package/dist/bridge/ids.js +5 -0
  57. package/dist/bridge/incomingType.d.ts +10 -0
  58. package/dist/bridge/incomingType.d.ts.map +1 -0
  59. package/dist/bridge/incomingType.js +17 -0
  60. package/dist/bridge/loadManifest.d.ts +25 -0
  61. package/dist/bridge/loadManifest.d.ts.map +1 -0
  62. package/dist/bridge/loadManifest.js +68 -0
  63. package/dist/bridge/manifest.d.ts +66 -0
  64. package/dist/bridge/manifest.d.ts.map +1 -0
  65. package/dist/bridge/manifest.js +40 -0
  66. package/dist/bridge/messages/createPage.d.ts +225 -0
  67. package/dist/bridge/messages/createPage.d.ts.map +1 -0
  68. package/dist/bridge/messages/createPage.js +46 -0
  69. package/dist/bridge/messages/createPageResult.d.ts +211 -0
  70. package/dist/bridge/messages/createPageResult.d.ts.map +1 -0
  71. package/dist/bridge/messages/createPageResult.js +28 -0
  72. package/dist/bridge/messages/currentUserChanged.d.ts +19 -0
  73. package/dist/bridge/messages/currentUserChanged.d.ts.map +1 -0
  74. package/dist/bridge/messages/currentUserChanged.js +9 -0
  75. package/dist/bridge/messages/dataSourcesChanged.d.ts +158 -0
  76. package/dist/bridge/messages/dataSourcesChanged.d.ts.map +1 -0
  77. package/dist/bridge/messages/dataSourcesChanged.js +13 -0
  78. package/dist/bridge/messages/getPage.d.ts +216 -0
  79. package/dist/bridge/messages/getPage.d.ts.map +1 -0
  80. package/dist/bridge/messages/getPage.js +33 -0
  81. package/dist/bridge/messages/getUser.d.ts +45 -0
  82. package/dist/bridge/messages/getUser.d.ts.map +1 -0
  83. package/dist/bridge/messages/getUser.js +32 -0
  84. package/dist/bridge/messages/hostToSandbox.d.ts +1089 -0
  85. package/dist/bridge/messages/hostToSandbox.d.ts.map +1 -0
  86. package/dist/bridge/messages/hostToSandbox.js +32 -0
  87. package/dist/bridge/messages/init.d.ts +209 -0
  88. package/dist/bridge/messages/init.d.ts.map +1 -0
  89. package/dist/bridge/messages/init.js +47 -0
  90. package/dist/bridge/messages/invalidHostMessage.d.ts +15 -0
  91. package/dist/bridge/messages/invalidHostMessage.d.ts.map +1 -0
  92. package/dist/bridge/messages/invalidHostMessage.js +13 -0
  93. package/dist/bridge/messages/invalidSandboxMessage.d.ts +15 -0
  94. package/dist/bridge/messages/invalidSandboxMessage.d.ts.map +1 -0
  95. package/dist/bridge/messages/invalidSandboxMessage.js +13 -0
  96. package/dist/bridge/messages/listUsers.d.ts +53 -0
  97. package/dist/bridge/messages/listUsers.d.ts.map +1 -0
  98. package/dist/bridge/messages/listUsers.js +33 -0
  99. package/dist/bridge/messages/pageChanged.d.ts +12 -0
  100. package/dist/bridge/messages/pageChanged.d.ts.map +1 -0
  101. package/dist/bridge/messages/pageChanged.js +9 -0
  102. package/dist/bridge/messages/parentChanged.d.ts +22 -0
  103. package/dist/bridge/messages/parentChanged.d.ts.map +1 -0
  104. package/dist/bridge/messages/parentChanged.js +9 -0
  105. package/dist/bridge/messages/queryDataSource.d.ts +20 -0
  106. package/dist/bridge/messages/queryDataSource.d.ts.map +1 -0
  107. package/dist/bridge/messages/queryDataSource.js +19 -0
  108. package/dist/bridge/messages/queryDataSourceResult.d.ts +90 -0
  109. package/dist/bridge/messages/queryDataSourceResult.d.ts.map +1 -0
  110. package/dist/bridge/messages/queryDataSourceResult.js +21 -0
  111. package/dist/bridge/messages/ready.d.ts +45 -0
  112. package/dist/bridge/messages/ready.d.ts.map +1 -0
  113. package/dist/bridge/messages/ready.js +37 -0
  114. package/dist/bridge/messages/resize.d.ts +12 -0
  115. package/dist/bridge/messages/resize.d.ts.map +1 -0
  116. package/dist/bridge/messages/resize.js +10 -0
  117. package/dist/bridge/messages/sandboxToHost.d.ts +410 -0
  118. package/dist/bridge/messages/sandboxToHost.d.ts.map +1 -0
  119. package/dist/bridge/messages/sandboxToHost.js +25 -0
  120. package/dist/bridge/messages/themeChanged.d.ts +11 -0
  121. package/dist/bridge/messages/themeChanged.d.ts.map +1 -0
  122. package/dist/bridge/messages/themeChanged.js +10 -0
  123. package/dist/bridge/messages/updatePage.d.ts +171 -0
  124. package/dist/bridge/messages/updatePage.d.ts.map +1 -0
  125. package/dist/bridge/messages/updatePage.js +15 -0
  126. package/dist/bridge/messages/updatePageResult.d.ts +210 -0
  127. package/dist/bridge/messages/updatePageResult.d.ts.map +1 -0
  128. package/dist/bridge/messages/updatePageResult.js +27 -0
  129. package/dist/bridge/pages/page.d.ts +600 -0
  130. package/dist/bridge/pages/page.d.ts.map +1 -0
  131. package/dist/bridge/pages/page.js +217 -0
  132. package/dist/bridge/parent.d.ts +25 -0
  133. package/dist/bridge/parent.d.ts.map +1 -0
  134. package/dist/bridge/parent.js +11 -0
  135. package/dist/bridge/pendingRequests.d.ts +14 -0
  136. package/dist/bridge/pendingRequests.d.ts.map +1 -0
  137. package/dist/bridge/pendingRequests.js +27 -0
  138. package/dist/bridge/sandboxClient.d.ts +63 -0
  139. package/dist/bridge/sandboxClient.d.ts.map +1 -0
  140. package/dist/bridge/sandboxClient.js +92 -0
  141. package/dist/bridge/theme.d.ts +4 -0
  142. package/dist/bridge/theme.d.ts.map +1 -0
  143. package/dist/bridge/theme.js +2 -0
  144. package/dist/bridge/users/user.d.ts +36 -0
  145. package/dist/bridge/users/user.d.ts.map +1 -0
  146. package/dist/bridge/users/user.js +19 -0
  147. package/dist/customBlock.d.ts +36 -0
  148. package/dist/customBlock.d.ts.map +1 -0
  149. package/dist/customBlock.js +65 -0
  150. package/dist/host/createCustomBlockHost.d.ts +34 -0
  151. package/dist/host/createCustomBlockHost.d.ts.map +1 -0
  152. package/dist/host/createCustomBlockHost.js +278 -0
  153. package/dist/host/lifecycle/initErrors.d.ts +10 -0
  154. package/dist/host/lifecycle/initErrors.d.ts.map +1 -0
  155. package/dist/host/lifecycle/initErrors.js +123 -0
  156. package/dist/host/lifecycle/ready.d.ts +8 -0
  157. package/dist/host/lifecycle/ready.d.ts.map +1 -0
  158. package/dist/host/lifecycle/ready.js +24 -0
  159. package/dist/host/lifecycle/types.d.ts +24 -0
  160. package/dist/host/lifecycle/types.d.ts.map +1 -0
  161. package/dist/host/lifecycle/types.js +1 -0
  162. package/dist/host/messages/invalidSandboxMessage.d.ts +7 -0
  163. package/dist/host/messages/invalidSandboxMessage.d.ts.map +1 -0
  164. package/dist/host/messages/invalidSandboxMessage.js +24 -0
  165. package/dist/host/messages/types.d.ts +60 -0
  166. package/dist/host/messages/types.d.ts.map +1 -0
  167. package/dist/host/messages/types.js +1 -0
  168. package/dist/host/queries/querySubscriptions.d.ts +20 -0
  169. package/dist/host/queries/querySubscriptions.d.ts.map +1 -0
  170. package/dist/host/queries/querySubscriptions.js +29 -0
  171. package/dist/host/queries/types.d.ts +9 -0
  172. package/dist/host/queries/types.d.ts.map +1 -0
  173. package/dist/host/queries/types.js +1 -0
  174. package/dist/host.d.ts +73 -0
  175. package/dist/host.d.ts.map +1 -0
  176. package/dist/host.js +42 -0
  177. package/dist/index.d.ts +37 -0
  178. package/dist/index.d.ts.map +1 -0
  179. package/dist/index.js +14 -0
  180. package/dist/init.d.ts +53 -0
  181. package/dist/init.d.ts.map +1 -0
  182. package/dist/init.js +87 -0
  183. package/dist/react/DebugMessageLog.d.ts +2 -0
  184. package/dist/react/DebugMessageLog.d.ts.map +1 -0
  185. package/dist/react/DebugMessageLog.js +22 -0
  186. package/dist/react/NotionCustomBlock.css +110 -0
  187. package/dist/react/NotionCustomBlock.d.ts +50 -0
  188. package/dist/react/NotionCustomBlock.d.ts.map +1 -0
  189. package/dist/react/NotionCustomBlock.js +81 -0
  190. package/dist/react/index.d.ts +14 -0
  191. package/dist/react/index.d.ts.map +1 -0
  192. package/dist/react/index.js +13 -0
  193. package/dist/react/standalonePreview.d.ts +2 -0
  194. package/dist/react/standalonePreview.d.ts.map +1 -0
  195. package/dist/react/standalonePreview.js +26 -0
  196. package/dist/react/useCustomBlockAutoResize.d.ts +30 -0
  197. package/dist/react/useCustomBlockAutoResize.d.ts.map +1 -0
  198. package/dist/react/useCustomBlockAutoResize.js +35 -0
  199. package/dist/react/useCustomBlockInit.d.ts +43 -0
  200. package/dist/react/useCustomBlockInit.d.ts.map +1 -0
  201. package/dist/react/useCustomBlockInit.js +60 -0
  202. package/dist/react/useDataSource.d.ts +13 -0
  203. package/dist/react/useDataSource.d.ts.map +1 -0
  204. package/dist/react/useDataSource.js +37 -0
  205. package/dist/react/useHostState.d.ts +2 -0
  206. package/dist/react/useHostState.d.ts.map +1 -0
  207. package/dist/react/useHostState.js +5 -0
  208. package/dist/react/useRuntimeState.d.ts +66 -0
  209. package/dist/react/useRuntimeState.d.ts.map +1 -0
  210. package/dist/react/useRuntimeState.js +73 -0
  211. package/dist/types.d.ts +154 -0
  212. package/dist/types.d.ts.map +1 -0
  213. package/dist/types.js +1 -0
  214. package/dist/utils.d.ts +9 -0
  215. package/dist/utils.d.ts.map +1 -0
  216. package/dist/utils.js +10 -0
  217. package/dist/version.d.ts +9 -0
  218. package/dist/version.d.ts.map +1 -0
  219. package/dist/version.js +7 -0
  220. package/docs/block-location.md +83 -0
  221. package/docs/data-sources.md +186 -0
  222. package/docs/lifecycle.md +172 -0
  223. package/docs/manifest.md +42 -0
  224. package/docs/pages.md +143 -0
  225. package/docs/users.md +92 -0
  226. package/package.json +69 -0
  227. package/src/autoResize.ts +84 -0
  228. package/src/bridge/SandboxBridge.ts +921 -0
  229. package/src/bridge/dataSources/dataSource.ts +63 -0
  230. package/src/bridge/dataSources/dataSourcePage.ts +71 -0
  231. package/src/bridge/dataSources/dataSourceValue.ts +19 -0
  232. package/src/bridge/dataSources/dateValue.ts +96 -0
  233. package/src/bridge/dataSources/errors.ts +14 -0
  234. package/src/bridge/dataSources/propertySchema.ts +186 -0
  235. package/src/bridge/dataSources/recordPointer.ts +13 -0
  236. package/src/bridge/dataSources/resolve.ts +99 -0
  237. package/src/bridge/dataSources/resolveProperty.ts +130 -0
  238. package/src/bridge/errors.ts +13 -0
  239. package/src/bridge/hostState.ts +154 -0
  240. package/src/bridge/ids.ts +58 -0
  241. package/src/bridge/incomingType.ts +19 -0
  242. package/src/bridge/loadManifest.ts +89 -0
  243. package/src/bridge/manifest.ts +53 -0
  244. package/src/bridge/messages/createPage.ts +67 -0
  245. package/src/bridge/messages/createPageResult.ts +52 -0
  246. package/src/bridge/messages/currentUserChanged.ts +14 -0
  247. package/src/bridge/messages/dataSourcesChanged.ts +18 -0
  248. package/src/bridge/messages/getPage.ts +52 -0
  249. package/src/bridge/messages/getUser.ts +48 -0
  250. package/src/bridge/messages/hostToSandbox.ts +37 -0
  251. package/src/bridge/messages/init.ts +81 -0
  252. package/src/bridge/messages/invalidHostMessage.ts +16 -0
  253. package/src/bridge/messages/invalidSandboxMessage.ts +18 -0
  254. package/src/bridge/messages/listUsers.ts +49 -0
  255. package/src/bridge/messages/pageChanged.ts +12 -0
  256. package/src/bridge/messages/parentChanged.ts +14 -0
  257. package/src/bridge/messages/queryDataSource.ts +24 -0
  258. package/src/bridge/messages/queryDataSourceResult.ts +38 -0
  259. package/src/bridge/messages/ready.ts +41 -0
  260. package/src/bridge/messages/resize.ts +13 -0
  261. package/src/bridge/messages/sandboxToHost.ts +30 -0
  262. package/src/bridge/messages/themeChanged.ts +15 -0
  263. package/src/bridge/messages/updatePage.ts +22 -0
  264. package/src/bridge/messages/updatePageResult.ts +49 -0
  265. package/src/bridge/pages/page.ts +281 -0
  266. package/src/bridge/parent.ts +27 -0
  267. package/src/bridge/pendingRequests.ts +28 -0
  268. package/src/bridge/sandboxClient.ts +137 -0
  269. package/src/bridge/theme.ts +5 -0
  270. package/src/bridge/users/user.ts +31 -0
  271. package/src/css.d.ts +1 -0
  272. package/src/customBlock.ts +102 -0
  273. package/src/host/createCustomBlockHost.ts +392 -0
  274. package/src/host/lifecycle/initErrors.ts +132 -0
  275. package/src/host/lifecycle/ready.ts +36 -0
  276. package/src/host/lifecycle/types.ts +28 -0
  277. package/src/host/messages/invalidSandboxMessage.ts +38 -0
  278. package/src/host/messages/types.ts +67 -0
  279. package/src/host/queries/querySubscriptions.ts +52 -0
  280. package/src/host/queries/types.ts +16 -0
  281. package/src/host.ts +167 -0
  282. package/src/index.ts +115 -0
  283. package/src/init.ts +143 -0
  284. package/src/react/DebugMessageLog.tsx +30 -0
  285. package/src/react/NotionCustomBlock.css +110 -0
  286. package/src/react/NotionCustomBlock.tsx +151 -0
  287. package/src/react/index.ts +28 -0
  288. package/src/react/standalonePreview.ts +28 -0
  289. package/src/react/useCustomBlockAutoResize.ts +46 -0
  290. package/src/react/useCustomBlockInit.ts +91 -0
  291. package/src/react/useDataSource.ts +47 -0
  292. package/src/react/useHostState.ts +9 -0
  293. package/src/react/useRuntimeState.ts +85 -0
  294. package/src/types.ts +175 -0
  295. package/src/utils.ts +13 -0
  296. package/src/version.ts +8 -0
  297. package/vite-plugin/index.d.ts +46 -0
  298. package/vite-plugin/index.js +115 -0
@@ -0,0 +1,921 @@
1
+ import * as v from "valibot"
2
+ import type {
3
+ CreatePageInput,
4
+ CreatePageResult,
5
+ GetPageResult,
6
+ GetUserResult,
7
+ ListUsersInput,
8
+ ListUsersResult,
9
+ NotionPageId,
10
+ NotionUserId,
11
+ UpdatePageInput,
12
+ UpdatePageResult,
13
+ UseDataSourceOptions,
14
+ } from "../types.js"
15
+ import { unreachable } from "../utils.js"
16
+ import { NCBLOCK_SDK_VERSION } from "../version.js"
17
+ import type {
18
+ NotionDataSource,
19
+ NotionDataSourceBindings,
20
+ } from "./dataSources/dataSource.js"
21
+ import type {
22
+ NotionDataSourcePageUpdateInput,
23
+ NotionDataSourcePageUpdateResult,
24
+ } from "./dataSources/dataSourcePage.js"
25
+ import { resolveDataSources } from "./dataSources/resolve.js"
26
+ import { resolvePropertyWriteMapForDataSource } from "./dataSources/resolveProperty.js"
27
+ import {
28
+ type CustomBlockHostState,
29
+ createEmptyDataSourceQueryState,
30
+ type DataSourceQueryState,
31
+ } from "./hostState.js"
32
+ import { readIncomingType } from "./incomingType.js"
33
+ import type { ManifestLoadResult } from "./loadManifest.js"
34
+ import type { CustomBlockManifest } from "./manifest.js"
35
+ import type {
36
+ CreatePageMessage,
37
+ CreatePageMessageParent,
38
+ } from "./messages/createPage.js"
39
+ import type { CustomBlockCreatePageErrorInfo } from "./messages/createPageResult.js"
40
+ import type { GetUserMessage } from "./messages/getUser.js"
41
+ import { hostToSandboxMessageSchema } from "./messages/hostToSandbox.js"
42
+ import { CustomBlockInitError, type InitMessage } from "./messages/init.js"
43
+ import type { InvalidHostMessage } from "./messages/invalidHostMessage.js"
44
+ import type { ListUsersMessage } from "./messages/listUsers.js"
45
+ import type { QueryDataSourceMessage } from "./messages/queryDataSource.js"
46
+ import type { CustomBlockQueryDataSourceErrorInfo } from "./messages/queryDataSourceResult.js"
47
+ import type { ReadyMessage } from "./messages/ready.js"
48
+ import type { ResizeMessage } from "./messages/resize.js"
49
+ import type { UpdatePageMessage } from "./messages/updatePage.js"
50
+ import { PendingRequests } from "./pendingRequests.js"
51
+ import type { NotionUser } from "./users/user.js"
52
+
53
+ /**
54
+ * Used to ensure that the host and client are using the same version of the bridge protocol. A
55
+ * single host needs to support multiple custom blocks built with different versions of the bridge
56
+ * protocol. Increment this number any time a breaking change is made to the bridge protocol.
57
+ */
58
+ export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 2
59
+
60
+ /**
61
+ * A single entry in the bridge message log. Kept intentionally plain so the log
62
+ * is copy-pasteable to a local coding agent without needing extra context.
63
+ */
64
+ export type MessageLogEntry = {
65
+ timestamp: string
66
+ direction: "sent" | "received"
67
+ data: unknown
68
+ }
69
+
70
+ export class SandboxBridge {
71
+ private hostState: CustomBlockHostState = {
72
+ status: "uninitialized",
73
+ theme: "light",
74
+ }
75
+ private listeners = new Set<() => void>()
76
+ private messageLog: MessageLogEntry[] = []
77
+ private messageLogListeners = new Set<() => void>()
78
+ private nextRequestId = 1
79
+ private readonly pendingCreatePage = new PendingRequests<CreatePageResult>(
80
+ "custom-block-create-page",
81
+ )
82
+ private readonly pendingGetPage = new PendingRequests<GetPageResult>(
83
+ "custom-block-get-page",
84
+ )
85
+ private readonly pendingGetUser = new PendingRequests<GetUserResult>(
86
+ "custom-block-get-user",
87
+ )
88
+ private readonly pendingListUsers = new PendingRequests<ListUsersResult>(
89
+ "custom-block-list-users",
90
+ )
91
+ private readonly pendingUpdatePage = new PendingRequests<UpdatePageResult>(
92
+ "custom-block-update-page",
93
+ )
94
+ private hasSentReady = false
95
+ private latestDataSourceBindings: NotionDataSourceBindings = {}
96
+ private resolveInit: ((message: InitMessage) => void) | undefined
97
+ private rejectInit: ((reason: Error) => void) | undefined
98
+ private readonly initMessage: Promise<InitMessage> = new Promise(
99
+ (resolve, reject) => {
100
+ this.resolveInit = resolve
101
+ this.rejectInit = reject
102
+ },
103
+ )
104
+ private manifest: CustomBlockManifest | null = null
105
+
106
+ constructor() {
107
+ // `ready` is sent later by `initCustomBlock` (after the manifest fetch
108
+ // resolves). Top-level / no-iframe rejection is handled there too, so
109
+ // the constructor just attaches the listener.
110
+ if (typeof window !== "undefined") {
111
+ window.addEventListener("message", this.handleMessage)
112
+ }
113
+ }
114
+
115
+ private static MAX_LOG_ENTRIES = 100
116
+
117
+ private logMessage(direction: "sent" | "received", data: unknown) {
118
+ if (this.messageLog.length >= SandboxBridge.MAX_LOG_ENTRIES) {
119
+ this.messageLog.shift()
120
+ }
121
+ this.messageLog.push({
122
+ timestamp: new Date().toISOString(),
123
+ direction,
124
+ data,
125
+ })
126
+ for (const listener of this.messageLogListeners) {
127
+ listener()
128
+ }
129
+ }
130
+
131
+ getMessageLog(): readonly MessageLogEntry[] {
132
+ return this.messageLog
133
+ }
134
+
135
+ subscribeToMessageLog(listener: () => void) {
136
+ this.messageLogListeners.add(listener)
137
+ return () => this.messageLogListeners.delete(listener)
138
+ }
139
+
140
+ awaitInit(signal?: AbortSignal): Promise<InitMessage> {
141
+ if (!signal) {
142
+ return this.initMessage
143
+ }
144
+ return new Promise((resolve, reject) => {
145
+ if (signal.aborted) {
146
+ reject(signal.reason)
147
+ return
148
+ }
149
+ const onAbort = () => reject(signal.reason)
150
+ signal.addEventListener("abort", onAbort, { once: true })
151
+ this.initMessage.then(
152
+ message => {
153
+ signal.removeEventListener("abort", onAbort)
154
+ resolve(message)
155
+ },
156
+ err => {
157
+ signal.removeEventListener("abort", onAbort)
158
+ reject(err)
159
+ },
160
+ )
161
+ })
162
+ }
163
+
164
+ sendReady(manifestResult: ManifestLoadResult) {
165
+ if (typeof window === "undefined") {
166
+ return
167
+ }
168
+ if (this.hasSentReady) {
169
+ console.warn("[notion-custom-sdk] ignoring duplicate ready message")
170
+ return
171
+ }
172
+ const { manifest, error } = manifestResult
173
+ this.hasSentReady = true
174
+ this.manifest = manifest
175
+ const readyMessage: ReadyMessage =
176
+ error !== undefined
177
+ ? {
178
+ type: "ready",
179
+ status: "error",
180
+ bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
181
+ sdkVersion: NCBLOCK_SDK_VERSION,
182
+ error,
183
+ }
184
+ : {
185
+ type: "ready",
186
+ status: "success",
187
+ bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
188
+ sdkVersion: NCBLOCK_SDK_VERSION,
189
+ manifest,
190
+ }
191
+ this.postToHost(readyMessage)
192
+ }
193
+
194
+ private postToHost(message: unknown) {
195
+ console.debug("[notion-custom-sdk] outbound postMessage", message)
196
+ this.logMessage("sent", message)
197
+ window.parent.postMessage(message, "*")
198
+ }
199
+
200
+ private notify = () => {
201
+ for (const listener of this.listeners) {
202
+ listener()
203
+ }
204
+ }
205
+
206
+ private handleMessage = (event: MessageEvent) => {
207
+ console.debug("[notion-custom-sdk] incoming postMessage", {
208
+ data: event.data,
209
+ fromParent: event.source === window.parent,
210
+ })
211
+ if (event.source !== window.parent) {
212
+ return
213
+ }
214
+ this.logMessage("received", event.data)
215
+
216
+ const parsed = v.safeParse(hostToSandboxMessageSchema, event.data)
217
+ if (!parsed.success) {
218
+ console.warn(
219
+ "[notion-custom-sdk] ignoring malformed host message",
220
+ parsed.issues,
221
+ )
222
+ const incomingType = readIncomingType(event.data)
223
+ // Loop guard: never NACK a NACK. Excludes both directions —
224
+ // `invalidSandboxMessage` is what the host normally sends, but
225
+ // a buggy host that echoes our own `invalidHostMessage` back
226
+ // would otherwise spin a NACK feedback loop.
227
+ if (
228
+ incomingType !== "invalidSandboxMessage" &&
229
+ incomingType !== "invalidHostMessage"
230
+ ) {
231
+ const nack: InvalidHostMessage = {
232
+ type: "invalidHostMessage",
233
+ reason: formatInvalidHostReason(incomingType, parsed.issues),
234
+ }
235
+ this.postToHost(nack)
236
+ }
237
+ return
238
+ }
239
+ const message = parsed.output
240
+
241
+ // If the host couldn't parse one of our outbound sandbox-to-host messages, it sends back
242
+ // an `invalidSandboxMessage` NACK with a human-readable `reason`. Log it for developer
243
+ // visibility and stop. We don't retry as the sandbox can't recover from a host-side parse
244
+ // failure on its own.
245
+ if (message.type === "invalidSandboxMessage") {
246
+ console.warn(
247
+ "[notion-custom-sdk] host reported invalid sandbox message:",
248
+ message.reason,
249
+ )
250
+ return
251
+ }
252
+
253
+ // `init` is the only message valid before initialization. Handle it up
254
+ // front so every later case can assume `status === "initialized"`.
255
+ if (message.type === "init") {
256
+ this.applyInit(message)
257
+ return
258
+ }
259
+
260
+ // Alias to keep TS's narrowed `InitializedHostState` across the switch
261
+ // below. Reading `this.hostState` repeatedly would re-widen it.
262
+ const hostState = this.hostState
263
+
264
+ if (hostState.status !== "initialized") {
265
+ console.warn(`[notion-custom-sdk] ignoring ${message.type} before init`)
266
+ return
267
+ }
268
+
269
+ switch (message.type) {
270
+ case "themeChanged": {
271
+ this.hostState = {
272
+ ...hostState,
273
+ theme: message.theme,
274
+ }
275
+ this.notify()
276
+ return
277
+ }
278
+
279
+ case "parentChanged": {
280
+ this.hostState = {
281
+ ...hostState,
282
+ parent: message.parent,
283
+ }
284
+ this.notify()
285
+ return
286
+ }
287
+
288
+ case "pageChanged": {
289
+ this.hostState = {
290
+ ...hostState,
291
+ page: message.page,
292
+ }
293
+ this.notify()
294
+ return
295
+ }
296
+
297
+ case "currentUserChanged": {
298
+ this.hostState = {
299
+ ...hostState,
300
+ currentUser: message.currentUser,
301
+ }
302
+ this.notify()
303
+ return
304
+ }
305
+
306
+ case "dataSourcesChanged": {
307
+ const nextBindings = message.dataSources.bindings
308
+ const dataSources = reuseDataSourcesForUnchangedBindings({
309
+ previousDataSources: hostState.dataSources,
310
+ previousBindings: this.latestDataSourceBindings,
311
+ nextDataSources: resolveDataSources({
312
+ manifest: this.manifest,
313
+ dataSourceBindings: nextBindings,
314
+ }),
315
+ nextBindings,
316
+ })
317
+ this.latestDataSourceBindings = nextBindings
318
+ // Drop cached query state for keys that no longer exist in the mapping.
319
+ const nextKeys = new Set(dataSources.map(s => s.key))
320
+ const prunedState: Record<string, DataSourceQueryState> = {}
321
+ for (const [key, state] of Object.entries(hostState.dataSourceState)) {
322
+ if (nextKeys.has(key)) {
323
+ prunedState[key] = state
324
+ }
325
+ }
326
+ this.hostState = {
327
+ ...hostState,
328
+ dataSources,
329
+ dataSourceState: prunedState,
330
+ }
331
+ this.notify()
332
+ return
333
+ }
334
+
335
+ case "createPageResult": {
336
+ const result: CreatePageResult =
337
+ message.status === "success"
338
+ ? { status: "success", page: message.page }
339
+ : { status: "error", error: message.error }
340
+ if (!this.pendingCreatePage.resolve(message.requestId, result)) {
341
+ console.warn(
342
+ `[notion-custom-sdk] createPageResult for unknown requestId ${message.requestId}`,
343
+ )
344
+ }
345
+ return
346
+ }
347
+
348
+ case "getPageResult": {
349
+ const result: GetPageResult =
350
+ message.status === "success"
351
+ ? { status: "success", page: message.page }
352
+ : { status: "error", error: message.error }
353
+ if (!this.pendingGetPage.resolve(message.requestId, result)) {
354
+ console.warn(
355
+ `[notion-custom-sdk] getPageResult for unknown requestId ${message.requestId}`,
356
+ )
357
+ }
358
+ return
359
+ }
360
+
361
+ case "getUserResult": {
362
+ const result: GetUserResult =
363
+ message.status === "success"
364
+ ? { status: "success", user: message.user as unknown as NotionUser }
365
+ : { status: "error", error: message.error }
366
+ if (!this.pendingGetUser.resolve(message.requestId, result)) {
367
+ console.warn(
368
+ `[notion-custom-sdk] getUserResult for unknown requestId ${message.requestId}`,
369
+ )
370
+ }
371
+ return
372
+ }
373
+
374
+ case "listUsersResult": {
375
+ const result: ListUsersResult =
376
+ message.status === "success"
377
+ ? { status: "success", list: message.list }
378
+ : { status: "error", error: message.error }
379
+ if (!this.pendingListUsers.resolve(message.requestId, result)) {
380
+ console.warn(
381
+ `[notion-custom-sdk] listUsersResult for unknown requestId ${message.requestId}`,
382
+ )
383
+ }
384
+ return
385
+ }
386
+
387
+ case "updatePageResult": {
388
+ const result: UpdatePageResult =
389
+ message.status === "success"
390
+ ? { status: "success", page: message.page }
391
+ : { status: "error", error: message.error }
392
+ if (!this.pendingUpdatePage.resolve(message.requestId, result)) {
393
+ console.warn(
394
+ `[notion-custom-sdk] updatePageResult for unknown requestId ${message.requestId}`,
395
+ )
396
+ }
397
+ return
398
+ }
399
+
400
+ case "queryDataSourceResult": {
401
+ const queryEntry = Object.entries(hostState.dataSourceState).find(
402
+ ([, state]) => state.latestSnapshotId === message.snapshotId,
403
+ )
404
+ if (queryEntry === undefined) {
405
+ return
406
+ }
407
+ const [key, currentState] = queryEntry
408
+ if (currentState.latestRequestId !== message.requestId) {
409
+ return
410
+ }
411
+ this.hostState = {
412
+ ...hostState,
413
+ dataSourceState: {
414
+ ...hostState.dataSourceState,
415
+ [key]: {
416
+ items: message.items,
417
+ isLoading: false,
418
+ hasMore: message.hasMore,
419
+ error: message.error,
420
+ // Keep the request ID so later host-pushed refreshes for the
421
+ // same subscription still match.
422
+ latestRequestId: message.requestId,
423
+ latestSnapshotId: message.snapshotId,
424
+ latestLimit: currentState.latestLimit,
425
+ },
426
+ },
427
+ }
428
+ this.notify()
429
+ return
430
+ }
431
+
432
+ default: {
433
+ unreachable(message)
434
+ }
435
+ }
436
+ }
437
+
438
+ subscribe(listener: () => void) {
439
+ this.listeners.add(listener)
440
+ return () => this.listeners.delete(listener)
441
+ }
442
+
443
+ getHostState(): CustomBlockHostState {
444
+ return this.hostState
445
+ }
446
+
447
+ /**
448
+ * The author-declared manifest loaded from `custom_blocks.json` and forwarded
449
+ * to the host in `ready`. `null` when it failed to load/parse and the host
450
+ * should reject init via `ready.status: "error"`. Static for the lifetime
451
+ * of the sandbox.
452
+ */
453
+ getManifest(): CustomBlockManifest | null {
454
+ return this.manifest
455
+ }
456
+
457
+ /**
458
+ * Apply an `init` payload as if it had arrived from the host. Lets callers
459
+ * seed the bridge directly (e.g. the React provider's standalone preview
460
+ * fallback) without going through `postMessage`. The bridge stays unaware
461
+ * of why it's being seeded.
462
+ */
463
+ setMockState(message: InitMessage) {
464
+ this.applyInit(message)
465
+ }
466
+
467
+ private applyInit(message: InitMessage) {
468
+ if (message.status === "error") {
469
+ // The host couldn't construct block location for this block (most commonly the parent record
470
+ // failed to resolve). Surface the failure through the `awaitInit` promise so callers see
471
+ // it through `useCustomBlockInit().error` instead of timing out. We also log here
472
+ // because not every consumer renders the error UI, and we want the failure visible in
473
+ // the browser console either way. We deliberately don't NACK the host or post anything
474
+ // back over the bridge since the host has already given up.
475
+ console.error(
476
+ `[notion-custom-sdk] host reported init error (${message.error.code}): ${message.error.message}`,
477
+ )
478
+ if (this.rejectInit) {
479
+ this.rejectInit(new CustomBlockInitError(message.error))
480
+ this.resolveInit = undefined
481
+ this.rejectInit = undefined
482
+ }
483
+ return
484
+ }
485
+
486
+ const { blockId, parent, page } = message
487
+
488
+ this.latestDataSourceBindings = message.dataSources.bindings
489
+ const dataSources = resolveDataSources({
490
+ manifest: this.manifest,
491
+ dataSourceBindings: this.latestDataSourceBindings,
492
+ })
493
+ this.hostState = {
494
+ status: "initialized",
495
+ theme: message.theme,
496
+ blockId,
497
+ parent,
498
+ page,
499
+ currentUser: message.currentUser,
500
+ dataSources,
501
+ dataSourceState: {},
502
+ }
503
+ this.notify()
504
+ // Resolve the awaitInit promise once. Subsequent `init` messages
505
+ // (the host shouldn't send these, but be tolerant) update state but
506
+ // don't re-resolve.
507
+ if (this.resolveInit) {
508
+ this.resolveInit(message)
509
+ this.resolveInit = undefined
510
+ this.rejectInit = undefined
511
+ }
512
+ }
513
+
514
+ queryDataSource(key: string, options: UseDataSourceOptions = {}) {
515
+ if (this.hostState.status !== "initialized") {
516
+ return
517
+ }
518
+
519
+ const dataSource = this.hostState.dataSources.find(
520
+ entry => entry.key === key,
521
+ )
522
+ const currentState =
523
+ this.hostState.dataSourceState[key] ?? createEmptyDataSourceQueryState()
524
+ if (dataSource === undefined) {
525
+ this.setDataSourceQueryError(key, currentState, {
526
+ code: "unknown_data_source_key",
527
+ message: `Unknown data source key "${key}". Known keys: [${this.hostState.dataSources.map(entry => entry.key).join(", ")}].`,
528
+ isRetryable: false,
529
+ })
530
+ return
531
+ }
532
+ if (dataSource.collectionPointer === undefined) {
533
+ this.setDataSourceQueryError(key, currentState, {
534
+ code: "unmapped_data_source",
535
+ message: `Data source "${key}" has not been mapped to a database yet.`,
536
+ isRetryable: false,
537
+ })
538
+ return
539
+ }
540
+
541
+ const limit = resolveDataSourceQueryLimit(options.limit)
542
+ const snapshotId = makeDataSourceSnapshotId({
543
+ key,
544
+ })
545
+
546
+ if (
547
+ currentState.isLoading &&
548
+ currentState.latestSnapshotId === snapshotId &&
549
+ currentState.latestLimit === limit
550
+ ) {
551
+ return
552
+ }
553
+
554
+ const requestId = `custom-block-query-${this.nextRequestId}`
555
+ this.nextRequestId += 1
556
+
557
+ this.hostState = {
558
+ ...this.hostState,
559
+ dataSourceState: {
560
+ ...this.hostState.dataSourceState,
561
+ [key]: {
562
+ ...currentState,
563
+ isLoading: true,
564
+ error: undefined,
565
+ latestRequestId: requestId,
566
+ latestSnapshotId: snapshotId,
567
+ latestLimit: limit,
568
+ },
569
+ },
570
+ }
571
+ this.notify()
572
+
573
+ const outbound: QueryDataSourceMessage = {
574
+ type: "queryDataSource",
575
+ requestId,
576
+ snapshotId,
577
+ dataSourceId: dataSource.collectionPointer.id,
578
+ limit,
579
+ }
580
+ this.postToHost(outbound)
581
+ }
582
+
583
+ private setDataSourceQueryError(
584
+ key: string,
585
+ currentState: DataSourceQueryState,
586
+ error: CustomBlockQueryDataSourceErrorInfo,
587
+ ) {
588
+ if (this.hostState.status !== "initialized") {
589
+ return
590
+ }
591
+ this.hostState = {
592
+ ...this.hostState,
593
+ dataSourceState: {
594
+ ...this.hostState.dataSourceState,
595
+ [key]: {
596
+ ...currentState,
597
+ isLoading: false,
598
+ error,
599
+ latestRequestId: undefined,
600
+ latestSnapshotId: undefined,
601
+ latestLimit: undefined,
602
+ },
603
+ },
604
+ }
605
+ this.notify()
606
+ }
607
+
608
+ postResize(height: number) {
609
+ if (typeof window === "undefined") {
610
+ return
611
+ }
612
+ const safeHeight =
613
+ Number.isFinite(height) && height >= 0 ? Math.ceil(height) : 0
614
+ const outbound: ResizeMessage = {
615
+ type: "resize",
616
+ height: safeHeight,
617
+ }
618
+ this.postToHost(outbound)
619
+ }
620
+
621
+ createPage(input: CreatePageInput): Promise<CreatePageResult> {
622
+ return new Promise(resolve => {
623
+ const resolvedParent = this.resolveCreatePageParent(input.parent)
624
+ if (resolvedParent.status === "error") {
625
+ resolve(resolvedParent)
626
+ return
627
+ }
628
+ const resolvedProperties = resolvePropertyWriteMapForDataSource({
629
+ dataSource: resolvedParent.dataSource,
630
+ properties: input.properties,
631
+ operationName: "createPage",
632
+ })
633
+ if (resolvedProperties.status === "error") {
634
+ resolve(resolvedProperties)
635
+ return
636
+ }
637
+ const requestId = this.pendingCreatePage.allocate(resolve)
638
+ const outbound: CreatePageMessage = {
639
+ type: "createPage",
640
+ requestId,
641
+ parent: resolvedParent.parent,
642
+ properties: resolvedProperties.properties,
643
+ }
644
+ if (input.icon !== undefined) {
645
+ outbound.icon = input.icon
646
+ }
647
+ if (input.cover !== undefined) {
648
+ outbound.cover = input.cover
649
+ }
650
+ if (input.position !== undefined) {
651
+ outbound.position = input.position
652
+ }
653
+ this.postToHost(outbound)
654
+ })
655
+ }
656
+
657
+ getPage(pageId: NotionPageId): Promise<GetPageResult> {
658
+ return new Promise(resolve => {
659
+ const requestId = this.pendingGetPage.allocate(resolve)
660
+ const outbound = {
661
+ type: "getPage",
662
+ requestId,
663
+ pageId,
664
+ }
665
+ this.postToHost(outbound)
666
+ })
667
+ }
668
+
669
+ getUser(userId: NotionUserId): Promise<GetUserResult> {
670
+ return new Promise(resolve => {
671
+ const requestId = this.pendingGetUser.allocate(resolve)
672
+ const outbound: GetUserMessage = {
673
+ type: "getUser",
674
+ requestId,
675
+ userId,
676
+ }
677
+ this.postToHost(outbound)
678
+ })
679
+ }
680
+
681
+ listUsers(input: ListUsersInput = {}): Promise<ListUsersResult> {
682
+ return new Promise(resolve => {
683
+ const requestId = this.pendingListUsers.allocate(resolve)
684
+ const outbound: ListUsersMessage = {
685
+ type: "listUsers",
686
+ requestId,
687
+ startCursor: input.startCursor,
688
+ pageSize: input.pageSize,
689
+ }
690
+ this.postToHost(outbound)
691
+ })
692
+ }
693
+
694
+ updatePage(input: UpdatePageInput): Promise<UpdatePageResult> {
695
+ return new Promise(resolve => {
696
+ if (
697
+ (input.properties === undefined ||
698
+ Object.keys(input.properties).length === 0) &&
699
+ input.icon === undefined &&
700
+ input.cover === undefined &&
701
+ input.archived === undefined
702
+ ) {
703
+ resolve({
704
+ status: "error",
705
+ error: {
706
+ code: "invalid_page_update",
707
+ message:
708
+ "updatePage requires at least one of: properties, icon, cover, archived.",
709
+ isRetryable: false,
710
+ },
711
+ })
712
+ return
713
+ }
714
+
715
+ const requestId = this.pendingUpdatePage.allocate(resolve)
716
+ const outbound: UpdatePageMessage = {
717
+ type: "updatePage",
718
+ requestId,
719
+ pageId: input.pageId,
720
+ }
721
+ if (input.properties !== undefined) {
722
+ outbound.properties = input.properties
723
+ }
724
+ if (input.icon !== undefined) {
725
+ outbound.icon = input.icon
726
+ }
727
+ if (input.cover !== undefined) {
728
+ outbound.cover = input.cover
729
+ }
730
+ if (input.archived !== undefined) {
731
+ outbound.archived = input.archived
732
+ }
733
+ this.postToHost(outbound)
734
+ })
735
+ }
736
+
737
+ /**
738
+ * Updates a page on a known data source, resolving any property keys against the data source's
739
+ * `propertyIdsByKey` before sending the bridge message. Used by the per-row `update` callback
740
+ * returned from {@link getDataSourceQueryView}.
741
+ */
742
+ updateDataSourcePage(args: {
743
+ dataSource: NotionDataSource
744
+ pageId: NotionPageId
745
+ input: NotionDataSourcePageUpdateInput
746
+ }): Promise<NotionDataSourcePageUpdateResult> {
747
+ const { dataSource, pageId, input } = args
748
+ const resolvedProperties =
749
+ input.properties === undefined
750
+ ? undefined
751
+ : resolvePropertyWriteMapForDataSource({
752
+ dataSource,
753
+ properties: input.properties,
754
+ operationName: "dataSourcePage.update",
755
+ })
756
+ if (resolvedProperties?.status === "error") {
757
+ return Promise.resolve(resolvedProperties)
758
+ }
759
+
760
+ return this.updatePage({
761
+ pageId,
762
+ properties: resolvedProperties?.properties,
763
+ icon: input.icon,
764
+ cover: input.cover,
765
+ archived: input.archived,
766
+ })
767
+ }
768
+
769
+ /**
770
+ * Translates the public `CreatePageInput["parent"]` into the bridge-native
771
+ * `CreatePageMessageParent`. The `data_source_key` variant is resolved sandbox-side against
772
+ * the data source mapping the host delivered in `init` / `dataSourcesChanged`.
773
+ */
774
+ private resolveCreatePageParent(parent: CreatePageInput["parent"]):
775
+ | {
776
+ status: "ok"
777
+ parent: CreatePageMessageParent
778
+ dataSource: NotionDataSource | undefined
779
+ }
780
+ | { status: "error"; error: CustomBlockCreatePageErrorInfo } {
781
+ switch (parent.type) {
782
+ case "page_id":
783
+ return { status: "ok", parent, dataSource: undefined }
784
+ case "data_source_id": {
785
+ const dataSource =
786
+ this.hostState.status === "initialized"
787
+ ? this.hostState.dataSources.find(
788
+ entry => entry.collectionPointer?.id === parent.data_source_id,
789
+ )
790
+ : undefined
791
+ return { status: "ok", parent, dataSource }
792
+ }
793
+ case "data_source_key": {
794
+ if (this.hostState.status !== "initialized") {
795
+ return {
796
+ status: "error",
797
+ error: {
798
+ code: "unknown_data_source_key",
799
+ message: `Cannot resolve data source key "${parent.key}" before the host has initialized the SDK.`,
800
+ isRetryable: false,
801
+ },
802
+ }
803
+ }
804
+ const dataSource = this.hostState.dataSources.find(
805
+ entry => entry.key === parent.key,
806
+ )
807
+ if (dataSource === undefined) {
808
+ return {
809
+ status: "error",
810
+ error: {
811
+ code: "unknown_data_source_key",
812
+ message: `Unknown data source key "${parent.key}". Known keys: [${this.hostState.dataSources.map(entry => entry.key).join(", ")}].`,
813
+ isRetryable: false,
814
+ },
815
+ }
816
+ }
817
+ if (dataSource.collectionPointer === undefined) {
818
+ return {
819
+ status: "error",
820
+ error: {
821
+ code: "unmapped_data_source",
822
+ message: `Data source "${parent.key}" has not been mapped to a database yet.`,
823
+ isRetryable: false,
824
+ },
825
+ }
826
+ }
827
+ return {
828
+ status: "ok",
829
+ parent: {
830
+ type: "data_source_id",
831
+ data_source_id: dataSource.collectionPointer.id,
832
+ },
833
+ dataSource,
834
+ }
835
+ }
836
+ default:
837
+ unreachable(parent)
838
+ }
839
+ }
840
+ }
841
+
842
+ /**
843
+ * `postMessage` cloning creates new objects even for bindings that did not change.
844
+ * Preserve their resolved object identity so React effects only run for changed bindings.
845
+ *
846
+ * This comparison runs when the host sends an update instead of on every component render.
847
+ * Bindings are validated JSON-like bridge payloads, so serialization is sufficient here.
848
+ */
849
+ function reuseDataSourcesForUnchangedBindings(args: {
850
+ previousDataSources: NotionDataSource[]
851
+ previousBindings: NotionDataSourceBindings
852
+ nextDataSources: NotionDataSource[]
853
+ nextBindings: NotionDataSourceBindings
854
+ }): NotionDataSource[] {
855
+ const previousDataSourcesByKey = new Map(
856
+ args.previousDataSources.map(dataSource => [dataSource.key, dataSource]),
857
+ )
858
+
859
+ return args.nextDataSources.map(dataSource => {
860
+ const previousDataSource = previousDataSourcesByKey.get(dataSource.key)
861
+ if (
862
+ previousDataSource !== undefined &&
863
+ JSON.stringify(args.previousBindings[dataSource.key]) ===
864
+ JSON.stringify(args.nextBindings[dataSource.key])
865
+ ) {
866
+ return previousDataSource
867
+ }
868
+ return dataSource
869
+ })
870
+ }
871
+
872
+ // The default number of items to return in a live snapshot response if no limit is provided.
873
+ const DEFAULT_DATA_SOURCE_QUERY_LIMIT = 20
874
+
875
+ // The maximum number of items to return in a single live snapshot response.
876
+ const MAX_DATA_SOURCE_QUERY_LIMIT = 999
877
+
878
+ function resolveDataSourceQueryLimit(limit: number | undefined): number {
879
+ if (limit === undefined) {
880
+ return DEFAULT_DATA_SOURCE_QUERY_LIMIT
881
+ }
882
+ if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1) {
883
+ console.warn(
884
+ `[notion-custom-sdk] useDataSource limit must be a positive integer; using ${DEFAULT_DATA_SOURCE_QUERY_LIMIT}.`,
885
+ )
886
+ return DEFAULT_DATA_SOURCE_QUERY_LIMIT
887
+ }
888
+ if (limit > MAX_DATA_SOURCE_QUERY_LIMIT) {
889
+ console.warn(
890
+ `[notion-custom-sdk] useDataSource limit is capped at ${MAX_DATA_SOURCE_QUERY_LIMIT}.`,
891
+ )
892
+ return MAX_DATA_SOURCE_QUERY_LIMIT
893
+ }
894
+ return limit
895
+ }
896
+
897
+ function makeDataSourceSnapshotId(args: { key: string }): string {
898
+ const { key } = args
899
+ return `data-source:${encodeURIComponent(key)}`
900
+ }
901
+
902
+ function formatInvalidHostReason(
903
+ incomingType: string | undefined,
904
+ issues: readonly v.BaseIssue<unknown>[],
905
+ ): string {
906
+ const labelled = incomingType
907
+ ? `host message of type "${incomingType}"`
908
+ : "host message"
909
+ const first = issues[0]
910
+ if (!first) {
911
+ return `Could not parse ${labelled}: unknown error`
912
+ }
913
+ const path =
914
+ first.path
915
+ ?.map(p => String(p.key ?? ""))
916
+ .filter(Boolean)
917
+ .join(".") ?? ""
918
+ const detail = path ? `${path}: ${first.message}` : first.message
919
+ const extra = issues.length > 1 ? ` (+${issues.length - 1} more)` : ""
920
+ return `Could not parse ${labelled}: ${detail}${extra}`
921
+ }