@notionhq/custom-blocks 0.1.46 → 0.1.48

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 CHANGED
@@ -113,10 +113,9 @@ Hosts that omit a contrast mode use standard contrast.
113
113
 
114
114
  `NotionTokenScopeProps` has one required `children` prop.
115
115
 
116
- The CSS variables apply within the scope, not globally.
117
- Place portal containers inside `<NotionTokenScope>`, or wrap the portal content in another `<NotionTokenScope>`.
118
- Framework-neutral renderers can read the current appearance with `customBlock.getTheme()` and `customBlock.getContrastMode()`.
119
- See [Block location and appearance](./docs/block-location.md).
116
+ CSS variables apply only within `<NotionTokenScope>`. Place portal containers inside that scope, or wrap the portal content in another `<NotionTokenScope>`.
117
+
118
+ Outside React, use `customBlock.getTheme()` and `customBlock.getContrastMode()` to read the current appearance, or `customBlock.subscribeToTheme()` and `customBlock.subscribeToContrastMode()` to follow changes. See [Block location and appearance](./docs/block-location.md) for details.
120
119
 
121
120
  ## Reference
122
121
 
@@ -2,7 +2,7 @@ import { DEFAULT_CONTRAST_MODE } from "../protocol/contrast.js";
2
2
  import { hostToSandboxMessageSchema, } from "../protocol/messages/hostToSandbox.js";
3
3
  import { readIncomingType } from "../protocol/messages/incomingType.js";
4
4
  import { CustomBlockInitializationError, initMessageSchema, } from "../protocol/messages/init.js";
5
- import { CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION } from "../protocol/protocolVersion.js";
5
+ import { LATEST_BRIDGE_PROTOCOL_VERSION } from "../protocol/protocolVersion.js";
6
6
  import * as v from "valibot";
7
7
  import { unreachable } from "../utils.js";
8
8
  import { CUSTOM_BLOCKS_SDK_VERSION } from "../version.js";
@@ -101,7 +101,7 @@ export class SandboxBridge {
101
101
  type: "connect",
102
102
  initializationId,
103
103
  status: "error",
104
- bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
104
+ bridgeProtocolVersion: LATEST_BRIDGE_PROTOCOL_VERSION,
105
105
  sdkVersion: CUSTOM_BLOCKS_SDK_VERSION,
106
106
  error,
107
107
  }
@@ -109,7 +109,7 @@ export class SandboxBridge {
109
109
  type: "connect",
110
110
  initializationId,
111
111
  status: "success",
112
- bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
112
+ bridgeProtocolVersion: LATEST_BRIDGE_PROTOCOL_VERSION,
113
113
  sdkVersion: CUSTOM_BLOCKS_SDK_VERSION,
114
114
  ...(manifest !== null ? { manifest } : {}),
115
115
  };
@@ -8,6 +8,7 @@ import type { NotionTheme } from "./protocol/theme.js";
8
8
  import type { NotionUser } from "./protocol/users/user.js";
9
9
  import { autoResize } from "./autoResize.js";
10
10
  import { subscribeToDataSource } from "./bridge/dataSources/subscribe.js";
11
+ /** @deprecated Use individual context types and getters after `initCustomBlock` resolves. */
11
12
  export type CustomBlockState = {
12
13
  status: "uninitialized";
13
14
  theme: NotionTheme;
@@ -22,20 +23,91 @@ export type CustomBlockState = {
22
23
  currentUser: NotionUser;
23
24
  dataSources: NotionDataSource[];
24
25
  };
26
+ type Unsubscribe = () => void;
25
27
  /**
26
- * Framework-neutral runtime state APIs for a custom block. Call `initCustomBlock`
27
- * before reading initialized values, and use `subscribe` to react to host pushes.
28
+ * Read block and app context with getters, or follow changes with subscriptions.
29
+ * Getters throw if called before `initCustomBlock()` completes. Subscriptions
30
+ * can be registered before initialization and deliver the initial value once
31
+ * initialization completes, followed by changes to that value.
32
+ *
33
+ * Each subscription returns an unsubscribe function. Call it when the
34
+ * subscription is no longer needed.
28
35
  */
29
36
  export declare const customBlock: {
30
- subscribe(listener: () => void): () => void;
37
+ /** @deprecated Use an individual subscription method, such as `subscribeToTheme()` or `subscribeToCurrentUser()`. */
38
+ subscribe(listener: () => void): Unsubscribe;
39
+ /** @deprecated Use an individual getter method, such as `getTheme()` or `getCurrentUser()`, after `initCustomBlock()` resolves. */
31
40
  getState(): CustomBlockState;
41
+ /**
42
+ * Get the current user viewing the block.
43
+ * Throws if called before SDK initialization completes.
44
+ */
32
45
  getCurrentUser(): NotionUser;
46
+ /**
47
+ * Subscribes to the current `currentUser` and future changes.
48
+ * Waits for initialization before delivering the initial value.
49
+ */
50
+ subscribeToCurrentUser(listener: (value: NotionUser) => void): Unsubscribe;
51
+ /**
52
+ * Get the host's current theme.
53
+ * Throws if called before SDK initialization completes.
54
+ */
33
55
  getTheme(): NotionTheme;
56
+ /**
57
+ * Subscribes to the current `theme` and future changes.
58
+ * Waits for initialization before delivering the initial value.
59
+ */
60
+ subscribeToTheme(listener: (value: NotionTheme) => void): Unsubscribe;
61
+ /**
62
+ * Get the host's current contrast mode.
63
+ * Throws if called before SDK initialization completes.
64
+ */
34
65
  getContrastMode(): NotionContrastMode;
66
+ /**
67
+ * Subscribes to the current `contrastMode` and future changes.
68
+ * Waits for initialization before delivering the initial value.
69
+ */
70
+ subscribeToContrastMode(listener: (value: NotionContrastMode) => void): Unsubscribe;
71
+ /**
72
+ * Get the custom block's ID. The ID is stable for the lifetime of the block.
73
+ * Throws if called before SDK initialization completes.
74
+ */
35
75
  getBlockId(): NotionBlockId;
76
+ /**
77
+ * Delivers the block ID once. The ID is stable for the lifetime of the block.
78
+ * Waits for initialization before delivering the initial value.
79
+ */
80
+ subscribeToBlockId(listener: (value: NotionBlockId) => void): Unsubscribe;
81
+ /**
82
+ * Get the custom block's parent.
83
+ * Throws if called before SDK initialization completes.
84
+ */
36
85
  getParent(): NotionParent;
86
+ /**
87
+ * Subscribes to the current `parent` and future changes.
88
+ * Waits for initialization before delivering the initial value.
89
+ */
90
+ subscribeToParent(listener: (value: NotionParent) => void): Unsubscribe;
91
+ /**
92
+ * Get the containing page and its parent.
93
+ * Throws if called before SDK initialization completes.
94
+ */
37
95
  getPage(): CustomBlockPage;
96
+ /**
97
+ * Subscribes to the current `page` and future changes.
98
+ * Waits for initialization before delivering the initial value.
99
+ */
100
+ subscribeToPage(listener: (value: CustomBlockPage) => void): Unsubscribe;
101
+ /**
102
+ * Get the block's declared data source manifest.
103
+ * Throws if called before SDK initialization completes.
104
+ */
38
105
  getManifest(): CustomBlockManifest;
106
+ /**
107
+ * Subscribes to the current `manifest` and future changes.
108
+ * Waits for initialization before delivering the initial value.
109
+ */
110
+ subscribeToManifest(listener: (value: CustomBlockManifest) => void): Unsubscribe;
39
111
  autoResize: typeof autoResize;
40
112
  /**
41
113
  * Registers a listener that receives the current query snapshot and later updates.
@@ -43,4 +115,5 @@ export declare const customBlock: {
43
115
  */
44
116
  subscribeToDataSource: typeof subscribeToDataSource;
45
117
  };
118
+ export {};
46
119
  //# sourceMappingURL=customBlock.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"customBlock.d.ts","sourceRoot":"","sources":["../src/customBlock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8CAA8C,CAAA;AACtF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4DAA4D,CAAA;AAClG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AAC5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8CAA8C,CAAA;AACvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gDAAgD,CAAA;AACrF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4CAA4C,CAAA;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2CAA2C,CAAA;AAC5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gDAAgD,CAAA;AAChF,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAA;AAOzE,MAAM,MAAM,gBAAgB,GACzB;IACA,MAAM,EAAE,eAAe,CAAA;IACvB,KAAK,EAAE,WAAW,CAAA;IAClB,YAAY,EAAE,kBAAkB,CAAA;CAC/B,GACD;IACA,MAAM,EAAE,aAAa,CAAA;IACrB,KAAK,EAAE,WAAW,CAAA;IAClB,YAAY,EAAE,kBAAkB,CAAA;IAChC,OAAO,EAAE,aAAa,CAAA;IACtB,MAAM,EAAE,YAAY,CAAA;IACpB,IAAI,EAAE,eAAe,CAAA;IACrB,WAAW,EAAE,UAAU,CAAA;IACvB,WAAW,EAAE,gBAAgB,EAAE,CAAA;CAC9B,CAAA;AAEJ;;;GAGG;AACH,eAAO,MAAM,WAAW;wBACH,MAAM,IAAI,GAAG,MAAM,IAAI;gBAI/B,gBAAgB;sBAIV,UAAU;gBAIhB,WAAW;uBAIJ,kBAAkB;kBAIvB,aAAa;iBAId,YAAY;eAId,eAAe;mBAIX,mBAAmB;;IAMlC;;;OAGG;;CAEH,CAAA"}
1
+ {"version":3,"file":"customBlock.d.ts","sourceRoot":"","sources":["../src/customBlock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8CAA8C,CAAA;AACtF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4DAA4D,CAAA;AAClG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AAC5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8CAA8C,CAAA;AACvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gDAAgD,CAAA;AACrF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4CAA4C,CAAA;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2CAA2C,CAAA;AAC5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gDAAgD,CAAA;AAChF,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAA;AAQzE,6FAA6F;AAC7F,MAAM,MAAM,gBAAgB,GACzB;IACA,MAAM,EAAE,eAAe,CAAA;IACvB,KAAK,EAAE,WAAW,CAAA;IAClB,YAAY,EAAE,kBAAkB,CAAA;CAC/B,GACD;IACA,MAAM,EAAE,aAAa,CAAA;IACrB,KAAK,EAAE,WAAW,CAAA;IAClB,YAAY,EAAE,kBAAkB,CAAA;IAChC,OAAO,EAAE,aAAa,CAAA;IACtB,MAAM,EAAE,YAAY,CAAA;IACpB,IAAI,EAAE,eAAe,CAAA;IACrB,WAAW,EAAE,UAAU,CAAA;IACvB,WAAW,EAAE,gBAAgB,EAAE,CAAA;CAC9B,CAAA;AAEJ,KAAK,WAAW,GAAG,MAAM,IAAI,CAAA;AAE7B;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW;IACvB,qHAAqH;wBACjG,MAAM,IAAI,GAAG,WAAW;IAI5C,mIAAmI;gBACvH,gBAAgB;IAI5B;;;OAGG;sBACe,UAAU;IAI5B;;;OAGG;qCAC8B,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,WAAW;IAI1E;;;OAGG;gBACS,WAAW;IAIvB;;;OAGG;+BACwB,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAAG,WAAW;IAIrE;;;OAGG;uBACgB,kBAAkB;IAIrC;;;OAGG;sCAEQ,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAC3C,WAAW;IAId;;;OAGG;kBACW,aAAa;IAI3B;;;OAGG;iCAC0B,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAAG,WAAW;IAIzE;;;OAGG;iBACU,YAAY;IAIzB;;;OAGG;gCACyB,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,WAAW;IAIvE;;;OAGG;eACQ,eAAe;IAI1B;;;OAGG;8BACuB,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,GAAG,WAAW;IAIxE;;;OAGG;mBACY,mBAAmB;IAIlC;;;OAGG;kCAEQ,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,GAC5C,WAAW;;IAMd;;;OAGG;;CAEH,CAAA"}
@@ -1,38 +1,123 @@
1
1
  import { autoResize } from "./autoResize.js";
2
2
  import { subscribeToDataSource } from "./bridge/dataSources/subscribe.js";
3
+ import { notifyListener } from "./bridge/notifyListener.js";
3
4
  import { customBlockHost } from "./bridge/sandboxClient.js";
4
5
  /**
5
- * Framework-neutral runtime state APIs for a custom block. Call `initCustomBlock`
6
- * before reading initialized values, and use `subscribe` to react to host pushes.
6
+ * Read block and app context with getters, or follow changes with subscriptions.
7
+ * Getters throw if called before `initCustomBlock()` completes. Subscriptions
8
+ * can be registered before initialization and deliver the initial value once
9
+ * initialization completes, followed by changes to that value.
10
+ *
11
+ * Each subscription returns an unsubscribe function. Call it when the
12
+ * subscription is no longer needed.
7
13
  */
8
14
  export const customBlock = {
15
+ /** @deprecated Use an individual subscription method, such as `subscribeToTheme()` or `subscribeToCurrentUser()`. */
9
16
  subscribe(listener) {
10
17
  return customBlockHost.subscribe(listener);
11
18
  },
19
+ /** @deprecated Use an individual getter method, such as `getTheme()` or `getCurrentUser()`, after `initCustomBlock()` resolves. */
12
20
  getState() {
13
21
  return toPublicState(customBlockHost.getState());
14
22
  },
23
+ /**
24
+ * Get the current user viewing the block.
25
+ * Throws if called before SDK initialization completes.
26
+ */
15
27
  getCurrentUser() {
16
28
  return getInitializedHostState("getCurrentUser").currentUser;
17
29
  },
30
+ /**
31
+ * Subscribes to the current `currentUser` and future changes.
32
+ * Waits for initialization before delivering the initial value.
33
+ */
34
+ subscribeToCurrentUser(listener) {
35
+ return subscribeToContext({ key: "currentUser", listener });
36
+ },
37
+ /**
38
+ * Get the host's current theme.
39
+ * Throws if called before SDK initialization completes.
40
+ */
18
41
  getTheme() {
19
42
  return getInitializedHostState("getTheme").theme;
20
43
  },
44
+ /**
45
+ * Subscribes to the current `theme` and future changes.
46
+ * Waits for initialization before delivering the initial value.
47
+ */
48
+ subscribeToTheme(listener) {
49
+ return subscribeToContext({ key: "theme", listener });
50
+ },
51
+ /**
52
+ * Get the host's current contrast mode.
53
+ * Throws if called before SDK initialization completes.
54
+ */
21
55
  getContrastMode() {
22
56
  return getInitializedHostState("getContrastMode").contrastMode;
23
57
  },
58
+ /**
59
+ * Subscribes to the current `contrastMode` and future changes.
60
+ * Waits for initialization before delivering the initial value.
61
+ */
62
+ subscribeToContrastMode(listener) {
63
+ return subscribeToContext({ key: "contrastMode", listener });
64
+ },
65
+ /**
66
+ * Get the custom block's ID. The ID is stable for the lifetime of the block.
67
+ * Throws if called before SDK initialization completes.
68
+ */
24
69
  getBlockId() {
25
70
  return getInitializedHostState("getBlockId").blockId;
26
71
  },
72
+ /**
73
+ * Delivers the block ID once. The ID is stable for the lifetime of the block.
74
+ * Waits for initialization before delivering the initial value.
75
+ */
76
+ subscribeToBlockId(listener) {
77
+ return subscribeToContext({ key: "blockId", listener });
78
+ },
79
+ /**
80
+ * Get the custom block's parent.
81
+ * Throws if called before SDK initialization completes.
82
+ */
27
83
  getParent() {
28
84
  return getInitializedHostState("getParent").parent;
29
85
  },
86
+ /**
87
+ * Subscribes to the current `parent` and future changes.
88
+ * Waits for initialization before delivering the initial value.
89
+ */
90
+ subscribeToParent(listener) {
91
+ return subscribeToContext({ key: "parent", listener });
92
+ },
93
+ /**
94
+ * Get the containing page and its parent.
95
+ * Throws if called before SDK initialization completes.
96
+ */
30
97
  getPage() {
31
98
  return getInitializedHostState("getPage").page;
32
99
  },
100
+ /**
101
+ * Subscribes to the current `page` and future changes.
102
+ * Waits for initialization before delivering the initial value.
103
+ */
104
+ subscribeToPage(listener) {
105
+ return subscribeToContext({ key: "page", listener });
106
+ },
107
+ /**
108
+ * Get the block's declared data source manifest.
109
+ * Throws if called before SDK initialization completes.
110
+ */
33
111
  getManifest() {
34
112
  return getInitializedHostState("getManifest").manifest;
35
113
  },
114
+ /**
115
+ * Subscribes to the current `manifest` and future changes.
116
+ * Waits for initialization before delivering the initial value.
117
+ */
118
+ subscribeToManifest(listener) {
119
+ return subscribeToContext({ key: "manifest", listener });
120
+ },
36
121
  autoResize,
37
122
  /**
38
123
  * Registers a listener that receives the current query snapshot and later updates.
@@ -74,3 +159,36 @@ function getInitializedHostState(methodName) {
74
159
  }
75
160
  return hostState;
76
161
  }
162
+ /**
163
+ * Subscribe to one context value. Wait for initialization before delivering
164
+ * the initial value, then deliver updates when that value changes.
165
+ * Return a function that stops delivery, including before initialization.
166
+ */
167
+ function subscribeToContext(args) {
168
+ const { key, listener } = args;
169
+ let active = true;
170
+ let delivered = false;
171
+ let previous;
172
+ const deliverIfChanged = () => {
173
+ const state = customBlockHost.getState();
174
+ if (!active || state.status !== "initialized") {
175
+ return;
176
+ }
177
+ const next = state[key];
178
+ // Use referential equality to keep the implementation simple.
179
+ // Callbacks can receive different objects with the same contents.
180
+ // We have not benchmarked this choice. Deep equality may also be suitable.
181
+ if (delivered && Object.is(previous, next)) {
182
+ return;
183
+ }
184
+ previous = next;
185
+ delivered = true;
186
+ notifyListener(() => listener(next));
187
+ };
188
+ const unsubscribe = customBlockHost.subscribe(deliverIfChanged);
189
+ deliverIfChanged();
190
+ return () => {
191
+ active = false;
192
+ unsubscribe();
193
+ };
194
+ }
@@ -5,9 +5,10 @@ const connectMessageCommonEntries = {
5
5
  type: v.literal("connect"),
6
6
  initializationId: v.string(),
7
7
  /**
8
- * Used to ensure that the host and client are using the same version of the bridge protocol. A
9
- * single host needs to support multiple custom blocks built with different versions of the bridge
10
- * protocol. Increment this number any time a breaking change is made to the bridge protocol.
8
+ * The bridge protocol version this sandbox uses to communicate with its host.
9
+ * Hosts control which versions they support and can support multiple versions
10
+ * at the same time. Support for older versions is eventually removed, so blocks
11
+ * may need to update their SDK to continue working.
11
12
  */
12
13
  bridgeProtocolVersion: v.number(),
13
14
  /**
@@ -16,7 +16,8 @@ export const initResultMessageSchema = v.variant("status", [
16
16
  initializationId: v.string(),
17
17
  status: v.literal("success"),
18
18
  /** Height of the initial rendered custom-block content, in CSS pixels. */
19
- // TODO(custom-blocks): Make this required when bumping bridge protocol version 4.
19
+ // Older SDKs can omit the height. Current SDKs always send it.
20
+ // TODO(custom-blocks): Make this required when MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION increases to 4.
20
21
  initialHeight: v.optional(v.pipe(v.number(), v.finite(), v.minValue(0))),
21
22
  }),
22
23
  v.object({
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Used to ensure that the host and client are using the same version of the bridge protocol. A
3
- * single host needs to support multiple custom blocks built with different versions of the bridge
4
- * protocol. Increment this number any time a breaking change is made to the bridge protocol.
2
+ * The protocol version that the current SDK sends in `connect`.
3
+ * Increase this value for breaking wire changes. Hosts can continue to accept
4
+ * older versions, so this value does not set the minimum or maximum they accept.
5
5
  */
6
- export declare const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 3;
6
+ export declare const LATEST_BRIDGE_PROTOCOL_VERSION = 3;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Used to ensure that the host and client are using the same version of the bridge protocol. A
3
- * single host needs to support multiple custom blocks built with different versions of the bridge
4
- * protocol. Increment this number any time a breaking change is made to the bridge protocol.
2
+ * The protocol version that the current SDK sends in `connect`.
3
+ * Increase this value for breaking wire changes. Hosts can continue to accept
4
+ * older versions, so this value does not set the minimum or maximum they accept.
5
5
  */
6
- export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 3;
6
+ export const LATEST_BRIDGE_PROTOCOL_VERSION = 3;
@@ -35,7 +35,7 @@ export declare function useParent(): NotionParent;
35
35
  */
36
36
  export declare function usePage(): CustomBlockPage;
37
37
  /**
38
- * Returns the host's current theme. Re-renders on every `themeChanged` message from the host.
38
+ * Returns the host's current theme. Re-renders when the theme changes.
39
39
  *
40
40
  * Throws if called before `initCustomBlock` has resolved.
41
41
  *
@@ -44,8 +44,7 @@ export declare function usePage(): CustomBlockPage;
44
44
  */
45
45
  export declare function useTheme(): NotionTheme;
46
46
  /**
47
- * Returns the host's contrast preference. Re-renders on every protocol-v3
48
- * `contrastModeChanged` message from the host.
47
+ * Returns the host's contrast preference. Re-renders when the contrast preference changes.
49
48
  *
50
49
  * Throws if called before `initCustomBlock` has resolved.
51
50
  *
@@ -54,8 +53,7 @@ export declare function useTheme(): NotionTheme;
54
53
  */
55
54
  export declare function useContrastMode(): NotionContrastMode;
56
55
  /**
57
- * Returns the viewing user's Notion profile. Re-renders whenever the host sends a
58
- * `currentUserChanged` message.
56
+ * Returns the viewing user's Notion profile. Re-renders when the profile data changes.
59
57
  *
60
58
  * Throws if called before `initCustomBlock` has resolved. `await` it before mounting.
61
59
  *
@@ -1 +1 @@
1
- {"version":3,"file":"useRuntimeState.d.ts","sourceRoot":"","sources":["../../src/react/useRuntimeState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8CAA8C,CAAA;AACtF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AAC5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8CAA8C,CAAA;AACvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gDAAgD,CAAA;AACrF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4CAA4C,CAAA;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2CAA2C,CAAA;AAC5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gDAAgD,CAAA;AAIhF;;;;;;;GAOG;AACH,wBAAgB,UAAU,IAAI,aAAa,CAE1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,IAAI,YAAY,CAExC;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,IAAI,eAAe,CAEzC;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,IAAI,WAAW,CAEtC;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,IAAI,kBAAkB,CAKpD;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,IAAI,UAAU,CAE3C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,IAAI,mBAAmB,CAEjD"}
1
+ {"version":3,"file":"useRuntimeState.d.ts","sourceRoot":"","sources":["../../src/react/useRuntimeState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8CAA8C,CAAA;AACtF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AAC5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8CAA8C,CAAA;AACvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gDAAgD,CAAA;AACrF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4CAA4C,CAAA;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2CAA2C,CAAA;AAC5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gDAAgD,CAAA;AAIhF;;;;;;;GAOG;AACH,wBAAgB,UAAU,IAAI,aAAa,CAK1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,IAAI,YAAY,CAKxC;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,IAAI,eAAe,CAEzC;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,IAAI,WAAW,CAKtC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,IAAI,kBAAkB,CAKpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,IAAI,UAAU,CAK3C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,IAAI,mBAAmB,CAKjD"}
@@ -9,7 +9,7 @@ import { customBlock } from "../customBlock.js";
9
9
  * const blockId = useBlockId()
10
10
  */
11
11
  export function useBlockId() {
12
- return useSyncExternalStore(customBlock.subscribe, customBlock.getBlockId);
12
+ return useSyncExternalStore(customBlock.subscribeToBlockId, customBlock.getBlockId);
13
13
  }
14
14
  /**
15
15
  * Returns the block's parent in the document tree. Re-renders when the host sends
@@ -21,7 +21,7 @@ export function useBlockId() {
21
21
  * const parent = useParent()
22
22
  */
23
23
  export function useParent() {
24
- return useSyncExternalStore(customBlock.subscribe, customBlock.getParent);
24
+ return useSyncExternalStore(customBlock.subscribeToParent, customBlock.getParent);
25
25
  }
26
26
  /**
27
27
  * Returns the nearest enclosing page ancestor. Re-renders when the host sends
@@ -33,10 +33,10 @@ export function useParent() {
33
33
  * const page = usePage()
34
34
  */
35
35
  export function usePage() {
36
- return useSyncExternalStore(customBlock.subscribe, customBlock.getPage);
36
+ return useSyncExternalStore(customBlock.subscribeToPage, customBlock.getPage);
37
37
  }
38
38
  /**
39
- * Returns the host's current theme. Re-renders on every `themeChanged` message from the host.
39
+ * Returns the host's current theme. Re-renders when the theme changes.
40
40
  *
41
41
  * Throws if called before `initCustomBlock` has resolved.
42
42
  *
@@ -44,11 +44,10 @@ export function usePage() {
44
44
  * const theme = useTheme()
45
45
  */
46
46
  export function useTheme() {
47
- return useSyncExternalStore(customBlock.subscribe, customBlock.getTheme);
47
+ return useSyncExternalStore(customBlock.subscribeToTheme, customBlock.getTheme);
48
48
  }
49
49
  /**
50
- * Returns the host's contrast preference. Re-renders on every protocol-v3
51
- * `contrastModeChanged` message from the host.
50
+ * Returns the host's contrast preference. Re-renders when the contrast preference changes.
52
51
  *
53
52
  * Throws if called before `initCustomBlock` has resolved.
54
53
  *
@@ -56,11 +55,10 @@ export function useTheme() {
56
55
  * const contrastMode = useContrastMode()
57
56
  */
58
57
  export function useContrastMode() {
59
- return useSyncExternalStore(customBlock.subscribe, customBlock.getContrastMode);
58
+ return useSyncExternalStore(customBlock.subscribeToContrastMode, customBlock.getContrastMode);
60
59
  }
61
60
  /**
62
- * Returns the viewing user's Notion profile. Re-renders whenever the host sends a
63
- * `currentUserChanged` message.
61
+ * Returns the viewing user's Notion profile. Re-renders when the profile data changes.
64
62
  *
65
63
  * Throws if called before `initCustomBlock` has resolved. `await` it before mounting.
66
64
  *
@@ -69,7 +67,7 @@ export function useContrastMode() {
69
67
  * console.log(me.id, me.name, me.person.email)
70
68
  */
71
69
  export function useCurrentUser() {
72
- return useSyncExternalStore(customBlock.subscribe, customBlock.getCurrentUser);
70
+ return useSyncExternalStore(customBlock.subscribeToCurrentUser, customBlock.getCurrentUser);
73
71
  }
74
72
  /**
75
73
  * Returns the manifest that the block declared and the host provided during initialization.
@@ -84,5 +82,5 @@ export function useCurrentUser() {
84
82
  * resolved property IDs and schemas.
85
83
  */
86
84
  export function useManifest() {
87
- return useSyncExternalStore(customBlock.subscribe, customBlock.getManifest);
85
+ return useSyncExternalStore(customBlock.subscribeToManifest, customBlock.getManifest);
88
86
  }
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  *
5
5
  * WARNING: Generated during SDK publish. Do not edit in the published package.
6
6
  */
7
- export const CUSTOM_BLOCKS_SDK_VERSION = "0.1.46"
7
+ export const CUSTOM_BLOCKS_SDK_VERSION = "0.1.48"
@@ -1,38 +1,59 @@
1
- # Block Location & Appearance
1
+ # Block location and appearance
2
2
 
3
- Every custom block runs inside a larger Notion document. These hooks expose the block's own ID, the container it sits inside, the nearest enclosing page ID, and the host's resolved display and contrast modes.
3
+ Block and app context describes where the block runs and the host's appearance. Use getters for current values, subscriptions for initial values and later changes, or React hooks in components.
4
4
 
5
- ## API
5
+ ## API overview
6
6
 
7
- The host bridge carries these values as explicit `blockId`, `parent`, and `page` fields.
7
+ Import `customBlock` from `@notionhq/custom-blocks` for getters and subscriptions. Import React hooks from `@notionhq/custom-blocks/react`.
8
8
 
9
- ### `useBlockId()`
9
+ The getter and subscription names below are methods on `customBlock`.
10
10
 
11
- Returns the custom block's own ID.
11
+ | Context | Getter | Subscription | React hook |
12
+ | --- | --- | --- | --- |
13
+ | Current user | `getCurrentUser()` | `subscribeToCurrentUser(callback)` | `useCurrentUser()` |
14
+ | Custom block ID | `getBlockId()` | `subscribeToBlockId(callback)` | `useBlockId()` |
15
+ | Parent | `getParent()` | `subscribeToParent(callback)` | `useParent()` |
16
+ | Containing page | `getPage()` | `subscribeToPage(callback)` | `usePage()` |
17
+ | Theme | `getTheme()` | `subscribeToTheme(callback)` | `useTheme()` |
18
+ | Contrast mode | `getContrastMode()` | `subscribeToContrastMode(callback)` | `useContrastMode()` |
19
+ | Manifest | `getManifest()` | `subscribeToManifest(callback)` | `useManifest()` |
12
20
 
13
- Re-renders when the host sends a block location update.
21
+ ## Reading values and following changes
14
22
 
15
- ```ts
16
- function useBlockId(): NotionBlockId;
17
- ```
23
+ Getters return the current value and throw before initialization completes. In React, call context hooks inside `NotionCustomBlock`, which waits for initialization before rendering its children.
18
24
 
19
- For non-React renderers, use `customBlock.getBlockId()` after `initCustomBlock()` resolves:
25
+ Subscriptions can register before initialization. They wait for initialization before delivering the initial value, or deliver immediately if initialization is complete. Start initialization with `initCustomBlock()` or `NotionCustomBlock`. Subscriptions do not start it themselves. These initialization APIs also report failures. Each subscription returns an unsubscribe function. Repeated cleanup calls have no effect.
26
+
27
+ After initial delivery, subscriptions compare values with `Object.is`. Objects use referential equality: a new reference triggers a callback even if its contents are unchanged. Updates to other context values do not trigger the callback. React hooks follow the same comparison rules.
20
28
 
21
29
  ```ts
22
- await initCustomBlock();
30
+ import { customBlock, initCustomBlock } from "@notionhq/custom-blocks"
31
+
32
+ const unsubscribe = customBlock.subscribeToTheme(theme => {
33
+ console.log(theme) // Initial theme, then later changes.
34
+ })
23
35
 
24
- const blockId = customBlock.getBlockId();
36
+ await initCustomBlock()
37
+ console.log(customBlock.getTheme()) // Read the current theme once.
38
+
39
+ // Call unsubscribe() when the view is removed.
25
40
  ```
26
41
 
27
- ### `useParent()`
42
+ ## Current user
28
43
 
29
- Returns the block's parent in the document tree.
44
+ Use `getCurrentUser()`, `subscribeToCurrentUser()`, and `useCurrentUser()` to access the viewing user's `NotionUser`. See [users](./users.md#reading-the-current-user) for profile fields and user APIs.
30
45
 
31
- Re-renders when the host sends a block location update.
46
+ ## Custom block ID
32
47
 
33
- ```ts
34
- function useParent(): NotionParent;
48
+ Use `getBlockId()`, `subscribeToBlockId()`, and `useBlockId()` to access the custom block's own `NotionBlockId`.
49
+
50
+ The ID is stable for the lifetime of the block. Each subscription delivers once, unless cancelled before initialization. You can register before initialization without an error.
35
51
 
52
+ ## Parent
53
+
54
+ Use `getParent()`, `subscribeToParent()`, and `useParent()` to access the block's `NotionParent` in the document tree. For a custom collection view, the parent identifies the data source associated with the view.
55
+
56
+ ```ts
36
57
  type NotionParent =
37
58
  | { type: "page_id"; page_id: string } // inline custom block under a page
38
59
  | { type: "block_id"; block_id: NotionBlockId } // inline custom block under a toggle/column/callout/...
@@ -42,8 +63,6 @@ type NotionParent =
42
63
  | { type: "unavailable" }; // access denied; no parent ID or kind
43
64
  ```
44
65
 
45
- For non-React renderers, use `customBlock.getParent()` after `initCustomBlock()` resolves.
46
-
47
66
  To identify custom blocks in agent-owned instruction content:
48
67
 
49
68
  ```tsx
@@ -60,15 +79,11 @@ export function AgentInstructionBadge() {
60
79
  }
61
80
  ```
62
81
 
63
- ### `usePage()`
64
-
65
- Returns the nearest enclosing `page` / `collection_view_page` ancestor.
82
+ ## Containing page
66
83
 
67
- Re-renders when the host sends a block location update including when the containing page itself moves (its `parent` changes) without the page ID changing.
84
+ Use `getPage()`, `subscribeToPage()`, and `usePage()` to access the nearest enclosing page or collection view page. The page's parent can change even when its ID stays the same.
68
85
 
69
86
  ```ts
70
- function usePage(): CustomBlockPage;
71
-
72
87
  type CustomBlockPage = {
73
88
  id: NotionPageId;
74
89
  parent: NotionParent; // the containing page's own parent
@@ -77,15 +92,10 @@ type CustomBlockPage = {
77
92
 
78
93
  `page.parent` allows determining if the custom block is in a freestanding page or part of a database. Don't confuse it with `useParent()`, which is the custom block's own parent.
79
94
 
80
- ```tsx
81
- const page = usePage();
82
- const isInsideDatabaseRow = page.parent.type === "data_source_id";
83
- ```
95
+ A containing page whose parent has `type: "data_source_id"` belongs to a data source.
84
96
 
85
97
  For anything beyond location — title, icon, properties — fetch the page through the pages API: `pages.get(page.id)`.
86
98
 
87
- For non-React renderers, use `customBlock.getPage()` after `initCustomBlock()` resolves.
88
-
89
99
  When the viewer cannot access the parent resource, the host returns
90
100
  `{ type: "unavailable" }`, including on `usePage().parent`. This applies to every
91
101
  parent kind, including data sources, even when the readable child identifies
@@ -97,25 +107,13 @@ Treat `unavailable` as unknown when branching on location: it does not establish
97
107
  whether the containing page is a database row. A parent that is still loading
98
108
  is not reported as access denied.
99
109
 
100
- ### `useTheme()`
101
-
102
- Returns the host's current theme.
110
+ ## Theme and contrast
103
111
 
104
- Re-renders on every `themeChanged` message.
105
-
106
- ```ts
107
- function useTheme(): NotionTheme; // "light" | "dark"
108
- ```
112
+ Use design tokens to style the block. When your code needs the current appearance, use a getter, subscription, or React hook from the [API overview](#api-overview).
109
113
 
110
- The bundled Notion design-token stylesheet maps this value to `data-display-mode`, not `data-theme`; `data-theme` selects content color palettes such as `blue`, `red`, and `gray`.
114
+ `NotionTheme` is `"light"` or `"dark"`. `NotionContrastMode` is `"standard"` or `"high"`. Hosts that omit a contrast mode use `"standard"`.
111
115
 
112
- For non-React renderers, use `customBlock.getTheme()`:
113
-
114
- ```ts
115
- await initCustomBlock();
116
-
117
- const theme = customBlock.getTheme();
118
- ```
116
+ Getters read the current value after initialization. Subscriptions can register earlier. They deliver the initial value once initialized, then later changes. React hooks keep components current when called inside `NotionCustomBlock`.
119
117
 
120
118
  ```tsx
121
119
  import { usePage, useTheme } from "@notionhq/custom-blocks/react";
@@ -127,14 +125,12 @@ export function Header() {
127
125
  }
128
126
  ```
129
127
 
130
- ### `useContrastMode()`
128
+ For styling, `NotionTokenScope` applies theme and contrast mode to the bundled Notion CSS variables automatically. The stylesheet uses `data-display-mode` for light and dark mode. The separate `data-theme` attribute selects content color palettes such as `blue`, `red`, and `gray`.
131
129
 
132
- Returns the host's resolved contrast mode. Hosts that omit it during initialization default to `"standard"`; later changes arrive through `contrastModeChanged`.
130
+ See [Reading values and following changes](#reading-values-and-following-changes) for a getter and subscription example, including cleanup.
133
131
 
134
- ```ts
135
- function useContrastMode(): NotionContrastMode; // "standard" | "high"
136
- ```
132
+ ## Manifest
137
133
 
138
- For non-React renderers, use `customBlock.getContrastMode()` after `initCustomBlock()` resolves.
134
+ Use `getManifest()`, `subscribeToManifest()`, and `useManifest()` to access the declared `CustomBlockManifest`. The manifest stays fixed for the lifetime of the running block, so each subscription delivers it once. You can register before initialization without an error.
139
135
 
140
- For React apps, `<NotionTokenScope>` applies both appearance values to the bundled Notion CSS variables automatically.
136
+ See [data sources](./data-sources.md) for manifest details and `subscribeToDataSource()` for query snapshots.
@@ -60,8 +60,6 @@ const unsubscribe = customBlock.subscribeToDataSource({
60
60
  },
61
61
  });
62
62
 
63
- // When the renderer unmounts:
64
- unsubscribe();
65
63
  ```
66
64
 
67
65
  To change the query or request more rows, unsubscribe and create a new subscription with different options.
@@ -235,7 +233,9 @@ Returns the block manifest received from the host during initialization. The man
235
233
  function customBlock.getManifest(): CustomBlockManifest;
236
234
  ```
237
235
 
238
- Framework-neutral getter for the same manifest returned by `useManifest()`. The manifest is static for the lifetime of the sandbox, so there is nothing to subscribe to.
236
+ Framework-neutral getter for the same manifest returned by `useManifest()`.
237
+ `customBlock.subscribeToManifest(callback)` follows the [context subscription contract](./block-location.md#reading-values-and-following-changes).
238
+ The manifest stays fixed for the lifetime of the running block, so each subscription delivers it once. You can register before initialization without an error.
239
239
 
240
240
  ## Example: querying a data source
241
241
 
package/docs/lifecycle.md CHANGED
@@ -143,20 +143,61 @@ All code types are open string unions, so keep a default branch. Use `isRetryabl
143
143
 
144
144
  ### `customBlock`
145
145
 
146
- Framework-neutral runtime APIs for renderers that do not use React hooks. `customBlock.getState()` returns a `CustomBlockState` snapshot that includes `theme` and `contrastMode` while hiding internal query cache details. All getters that read host state — `getTheme`, `getContrastMode`, `getBlockId`, `getParent`, `getPage`, `getCurrentUser`, and `getManifest` — throw until `initCustomBlock()` resolves. `getManifest()` then returns the authoritative manifest supplied by the host.
146
+ The SDK provides getters, subscriptions, and React hooks for block and app context. Use `customBlock` for getters and subscriptions.
147
147
 
148
- `customBlock` covers runtime state, live data source snapshots, and sizing. Imperative APIs such as `pages.*` and `users.*` are also framework-neutral functions.
148
+ #### Getter
149
+
150
+ Getters return the current value, such as the theme, current user, or containing page. They throw if called before initialization completes.
149
151
 
150
152
  ```ts
151
- await initCustomBlock();
153
+ import { customBlock, initCustomBlock } from "@notionhq/custom-blocks"
152
154
 
153
- const theme = customBlock.getTheme();
154
- const contrastMode = customBlock.getContrastMode();
155
- const unsubscribe = customBlock.subscribe(() => {
156
- render(customBlock.getState());
157
- });
155
+ await initCustomBlock()
156
+ const theme = customBlock.getTheme()
157
+ console.log(theme)
158
+ ```
159
+
160
+ #### Subscription
161
+
162
+ Subscriptions deliver the initial value and later changes. You can register them before initialization. They wait for initialization before delivering the initial value, or deliver immediately if initialization is complete.
163
+
164
+ ```ts
165
+ import { customBlock, initCustomBlock } from "@notionhq/custom-blocks"
166
+
167
+ const unsubscribe = customBlock.subscribeToTheme(theme => {
168
+ console.log(theme)
169
+ })
170
+
171
+ await initCustomBlock()
172
+
173
+ // Call unsubscribe() when the view is removed to stop future callbacks.
158
174
  ```
159
175
 
176
+ #### React hook
177
+
178
+ In React, use context hooks inside `NotionCustomBlock`. The wrapper waits for initialization, and the hooks keep components current as values change. React handles subscription cleanup when the component unmounts.
179
+
180
+ ```tsx
181
+ import { NotionCustomBlock, useTheme } from "@notionhq/custom-blocks/react"
182
+
183
+ export function App() {
184
+ return (
185
+ <NotionCustomBlock>
186
+ <ThemeLabel />
187
+ </NotionCustomBlock>
188
+ )
189
+ }
190
+
191
+ function ThemeLabel() {
192
+ const theme = useTheme()
193
+ return <p>Current theme: {theme}</p>
194
+ }
195
+ ```
196
+
197
+ See [Block location and appearance](./block-location.md#reading-values-and-following-changes) for the available getters, subscriptions, and React hooks.
198
+
199
+ ### `customBlock.autoResize()`
200
+
160
201
  For non-React auto-resize, pass the element whose content height should drive the host iframe. The helper posts one initial measurement, observes later size changes when `ResizeObserver` is available, dedupes unchanged heights, and returns a cleanup function:
161
202
 
162
203
  ```ts
@@ -164,8 +205,7 @@ const stopAutoResize = customBlock.autoResize({
164
205
  target: document.getElementById("root"),
165
206
  });
166
207
 
167
- // Later, if your renderer unmounts:
168
- stopAutoResize();
208
+ // Call stopAutoResize() when your renderer is removed.
169
209
  ```
170
210
 
171
211
  ### `NotInIframeError`
package/docs/users.md CHANGED
@@ -29,14 +29,15 @@ For non-React renderers, read the same state through `customBlock` after initial
29
29
 
30
30
  ```ts
31
31
  await initCustomBlock()
32
- const me = customBlock.getCurrentUser()
33
32
 
34
- const unsubscribe = customBlock.subscribe(() => {
35
- const nextMe = customBlock.getCurrentUser()
33
+ const unsubscribe = customBlock.subscribeToCurrentUser(nextMe => {
36
34
  // Update your renderer with nextMe.
37
35
  })
38
36
  ```
39
37
 
38
+ Call `unsubscribe()` when your renderer is removed. The callback receives the initial profile and later changes.
39
+ See [context subscriptions](./block-location.md#reading-values-and-following-changes) for initialization and equality rules.
40
+
40
41
  ## Listing users
41
42
 
42
43
  `users.list(input?)` returns workspace users visible to the current custom block, mirroring Notion's [`GET /v1/users`](https://developers.notion.com/reference/get-users) shape.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/custom-blocks",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -34,7 +34,7 @@ import type { CustomBlockQueryDataSourceErrorInfo } from "@notionhq/custom-block
34
34
  import type { ResizeMessage } from "@notionhq/custom-blocks-protocol/messages/resize.js"
35
35
  import type { UnsubscribeDataSourceQueryMessage } from "@notionhq/custom-blocks-protocol/messages/unsubscribeDataSourceQuery.js"
36
36
  import type { UpdatePageMessage } from "@notionhq/custom-blocks-protocol/messages/updatePage.js"
37
- import { CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION } from "@notionhq/custom-blocks-protocol/protocolVersion.js"
37
+ import { LATEST_BRIDGE_PROTOCOL_VERSION } from "@notionhq/custom-blocks-protocol/protocolVersion.js"
38
38
  import type { NotionTheme } from "@notionhq/custom-blocks-protocol/theme.js"
39
39
  import * as v from "valibot"
40
40
  import type {
@@ -196,7 +196,7 @@ export class SandboxBridge {
196
196
  type: "connect",
197
197
  initializationId,
198
198
  status: "error",
199
- bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
199
+ bridgeProtocolVersion: LATEST_BRIDGE_PROTOCOL_VERSION,
200
200
  sdkVersion: CUSTOM_BLOCKS_SDK_VERSION,
201
201
  error,
202
202
  }
@@ -204,7 +204,7 @@ export class SandboxBridge {
204
204
  type: "connect",
205
205
  initializationId,
206
206
  status: "success",
207
- bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
207
+ bridgeProtocolVersion: LATEST_BRIDGE_PROTOCOL_VERSION,
208
208
  sdkVersion: CUSTOM_BLOCKS_SDK_VERSION,
209
209
  ...(manifest !== null ? { manifest } : {}),
210
210
  }
@@ -12,8 +12,10 @@ import type {
12
12
  CustomBlockHostState,
13
13
  InitializedHostState,
14
14
  } from "./bridge/hostState.js"
15
+ import { notifyListener } from "./bridge/notifyListener.js"
15
16
  import { customBlockHost } from "./bridge/sandboxClient.js"
16
17
 
18
+ /** @deprecated Use individual context types and getters after `initCustomBlock` resolves. */
17
19
  export type CustomBlockState =
18
20
  | {
19
21
  status: "uninitialized"
@@ -31,47 +33,144 @@ export type CustomBlockState =
31
33
  dataSources: NotionDataSource[]
32
34
  }
33
35
 
36
+ type Unsubscribe = () => void
37
+
34
38
  /**
35
- * Framework-neutral runtime state APIs for a custom block. Call `initCustomBlock`
36
- * before reading initialized values, and use `subscribe` to react to host pushes.
39
+ * Read block and app context with getters, or follow changes with subscriptions.
40
+ * Getters throw if called before `initCustomBlock()` completes. Subscriptions
41
+ * can be registered before initialization and deliver the initial value once
42
+ * initialization completes, followed by changes to that value.
43
+ *
44
+ * Each subscription returns an unsubscribe function. Call it when the
45
+ * subscription is no longer needed.
37
46
  */
38
47
  export const customBlock = {
39
- subscribe(listener: () => void): () => void {
48
+ /** @deprecated Use an individual subscription method, such as `subscribeToTheme()` or `subscribeToCurrentUser()`. */
49
+ subscribe(listener: () => void): Unsubscribe {
40
50
  return customBlockHost.subscribe(listener)
41
51
  },
42
52
 
53
+ /** @deprecated Use an individual getter method, such as `getTheme()` or `getCurrentUser()`, after `initCustomBlock()` resolves. */
43
54
  getState(): CustomBlockState {
44
55
  return toPublicState(customBlockHost.getState())
45
56
  },
46
57
 
58
+ /**
59
+ * Get the current user viewing the block.
60
+ * Throws if called before SDK initialization completes.
61
+ */
47
62
  getCurrentUser(): NotionUser {
48
63
  return getInitializedHostState("getCurrentUser").currentUser
49
64
  },
50
65
 
66
+ /**
67
+ * Subscribes to the current `currentUser` and future changes.
68
+ * Waits for initialization before delivering the initial value.
69
+ */
70
+ subscribeToCurrentUser(listener: (value: NotionUser) => void): Unsubscribe {
71
+ return subscribeToContext({ key: "currentUser", listener })
72
+ },
73
+
74
+ /**
75
+ * Get the host's current theme.
76
+ * Throws if called before SDK initialization completes.
77
+ */
51
78
  getTheme(): NotionTheme {
52
79
  return getInitializedHostState("getTheme").theme
53
80
  },
54
81
 
82
+ /**
83
+ * Subscribes to the current `theme` and future changes.
84
+ * Waits for initialization before delivering the initial value.
85
+ */
86
+ subscribeToTheme(listener: (value: NotionTheme) => void): Unsubscribe {
87
+ return subscribeToContext({ key: "theme", listener })
88
+ },
89
+
90
+ /**
91
+ * Get the host's current contrast mode.
92
+ * Throws if called before SDK initialization completes.
93
+ */
55
94
  getContrastMode(): NotionContrastMode {
56
95
  return getInitializedHostState("getContrastMode").contrastMode
57
96
  },
58
97
 
98
+ /**
99
+ * Subscribes to the current `contrastMode` and future changes.
100
+ * Waits for initialization before delivering the initial value.
101
+ */
102
+ subscribeToContrastMode(
103
+ listener: (value: NotionContrastMode) => void,
104
+ ): Unsubscribe {
105
+ return subscribeToContext({ key: "contrastMode", listener })
106
+ },
107
+
108
+ /**
109
+ * Get the custom block's ID. The ID is stable for the lifetime of the block.
110
+ * Throws if called before SDK initialization completes.
111
+ */
59
112
  getBlockId(): NotionBlockId {
60
113
  return getInitializedHostState("getBlockId").blockId
61
114
  },
62
115
 
116
+ /**
117
+ * Delivers the block ID once. The ID is stable for the lifetime of the block.
118
+ * Waits for initialization before delivering the initial value.
119
+ */
120
+ subscribeToBlockId(listener: (value: NotionBlockId) => void): Unsubscribe {
121
+ return subscribeToContext({ key: "blockId", listener })
122
+ },
123
+
124
+ /**
125
+ * Get the custom block's parent.
126
+ * Throws if called before SDK initialization completes.
127
+ */
63
128
  getParent(): NotionParent {
64
129
  return getInitializedHostState("getParent").parent
65
130
  },
66
131
 
132
+ /**
133
+ * Subscribes to the current `parent` and future changes.
134
+ * Waits for initialization before delivering the initial value.
135
+ */
136
+ subscribeToParent(listener: (value: NotionParent) => void): Unsubscribe {
137
+ return subscribeToContext({ key: "parent", listener })
138
+ },
139
+
140
+ /**
141
+ * Get the containing page and its parent.
142
+ * Throws if called before SDK initialization completes.
143
+ */
67
144
  getPage(): CustomBlockPage {
68
145
  return getInitializedHostState("getPage").page
69
146
  },
70
147
 
148
+ /**
149
+ * Subscribes to the current `page` and future changes.
150
+ * Waits for initialization before delivering the initial value.
151
+ */
152
+ subscribeToPage(listener: (value: CustomBlockPage) => void): Unsubscribe {
153
+ return subscribeToContext({ key: "page", listener })
154
+ },
155
+
156
+ /**
157
+ * Get the block's declared data source manifest.
158
+ * Throws if called before SDK initialization completes.
159
+ */
71
160
  getManifest(): CustomBlockManifest {
72
161
  return getInitializedHostState("getManifest").manifest
73
162
  },
74
163
 
164
+ /**
165
+ * Subscribes to the current `manifest` and future changes.
166
+ * Waits for initialization before delivering the initial value.
167
+ */
168
+ subscribeToManifest(
169
+ listener: (value: CustomBlockManifest) => void,
170
+ ): Unsubscribe {
171
+ return subscribeToContext({ key: "manifest", listener })
172
+ },
173
+
75
174
  autoResize,
76
175
 
77
176
  /**
@@ -119,3 +218,40 @@ function getInitializedHostState(methodName: string): InitializedHostState {
119
218
  }
120
219
  return hostState
121
220
  }
221
+
222
+ /**
223
+ * Subscribe to one context value. Wait for initialization before delivering
224
+ * the initial value, then deliver updates when that value changes.
225
+ * Return a function that stops delivery, including before initialization.
226
+ */
227
+ function subscribeToContext<K extends keyof InitializedHostState>(args: {
228
+ key: K
229
+ listener: (value: InitializedHostState[K]) => void
230
+ }): Unsubscribe {
231
+ const { key, listener } = args
232
+ let active = true
233
+ let delivered = false
234
+ let previous: InitializedHostState[K] | undefined
235
+ const deliverIfChanged = () => {
236
+ const state = customBlockHost.getState()
237
+ if (!active || state.status !== "initialized") {
238
+ return
239
+ }
240
+ const next = state[key]
241
+ // Use referential equality to keep the implementation simple.
242
+ // Callbacks can receive different objects with the same contents.
243
+ // We have not benchmarked this choice. Deep equality may also be suitable.
244
+ if (delivered && Object.is(previous, next)) {
245
+ return
246
+ }
247
+ previous = next
248
+ delivered = true
249
+ notifyListener(() => listener(next))
250
+ }
251
+ const unsubscribe = customBlockHost.subscribe(deliverIfChanged)
252
+ deliverIfChanged()
253
+ return () => {
254
+ active = false
255
+ unsubscribe()
256
+ }
257
+ }
@@ -17,7 +17,10 @@ import { customBlock } from "../customBlock.js"
17
17
  * const blockId = useBlockId()
18
18
  */
19
19
  export function useBlockId(): NotionBlockId {
20
- return useSyncExternalStore(customBlock.subscribe, customBlock.getBlockId)
20
+ return useSyncExternalStore(
21
+ customBlock.subscribeToBlockId,
22
+ customBlock.getBlockId,
23
+ )
21
24
  }
22
25
 
23
26
  /**
@@ -30,7 +33,10 @@ export function useBlockId(): NotionBlockId {
30
33
  * const parent = useParent()
31
34
  */
32
35
  export function useParent(): NotionParent {
33
- return useSyncExternalStore(customBlock.subscribe, customBlock.getParent)
36
+ return useSyncExternalStore(
37
+ customBlock.subscribeToParent,
38
+ customBlock.getParent,
39
+ )
34
40
  }
35
41
 
36
42
  /**
@@ -43,11 +49,11 @@ export function useParent(): NotionParent {
43
49
  * const page = usePage()
44
50
  */
45
51
  export function usePage(): CustomBlockPage {
46
- return useSyncExternalStore(customBlock.subscribe, customBlock.getPage)
52
+ return useSyncExternalStore(customBlock.subscribeToPage, customBlock.getPage)
47
53
  }
48
54
 
49
55
  /**
50
- * Returns the host's current theme. Re-renders on every `themeChanged` message from the host.
56
+ * Returns the host's current theme. Re-renders when the theme changes.
51
57
  *
52
58
  * Throws if called before `initCustomBlock` has resolved.
53
59
  *
@@ -55,12 +61,14 @@ export function usePage(): CustomBlockPage {
55
61
  * const theme = useTheme()
56
62
  */
57
63
  export function useTheme(): NotionTheme {
58
- return useSyncExternalStore(customBlock.subscribe, customBlock.getTheme)
64
+ return useSyncExternalStore(
65
+ customBlock.subscribeToTheme,
66
+ customBlock.getTheme,
67
+ )
59
68
  }
60
69
 
61
70
  /**
62
- * Returns the host's contrast preference. Re-renders on every protocol-v3
63
- * `contrastModeChanged` message from the host.
71
+ * Returns the host's contrast preference. Re-renders when the contrast preference changes.
64
72
  *
65
73
  * Throws if called before `initCustomBlock` has resolved.
66
74
  *
@@ -69,14 +77,13 @@ export function useTheme(): NotionTheme {
69
77
  */
70
78
  export function useContrastMode(): NotionContrastMode {
71
79
  return useSyncExternalStore(
72
- customBlock.subscribe,
80
+ customBlock.subscribeToContrastMode,
73
81
  customBlock.getContrastMode,
74
82
  )
75
83
  }
76
84
 
77
85
  /**
78
- * Returns the viewing user's Notion profile. Re-renders whenever the host sends a
79
- * `currentUserChanged` message.
86
+ * Returns the viewing user's Notion profile. Re-renders when the profile data changes.
80
87
  *
81
88
  * Throws if called before `initCustomBlock` has resolved. `await` it before mounting.
82
89
  *
@@ -85,7 +92,10 @@ export function useContrastMode(): NotionContrastMode {
85
92
  * console.log(me.id, me.name, me.person.email)
86
93
  */
87
94
  export function useCurrentUser(): NotionUser {
88
- return useSyncExternalStore(customBlock.subscribe, customBlock.getCurrentUser)
95
+ return useSyncExternalStore(
96
+ customBlock.subscribeToCurrentUser,
97
+ customBlock.getCurrentUser,
98
+ )
89
99
  }
90
100
 
91
101
  /**
@@ -101,5 +111,8 @@ export function useCurrentUser(): NotionUser {
101
111
  * resolved property IDs and schemas.
102
112
  */
103
113
  export function useManifest(): CustomBlockManifest {
104
- return useSyncExternalStore(customBlock.subscribe, customBlock.getManifest)
114
+ return useSyncExternalStore(
115
+ customBlock.subscribeToManifest,
116
+ customBlock.getManifest,
117
+ )
105
118
  }