@notionhq/custom-blocks 0.1.32 → 0.1.34

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 (38) hide show
  1. package/dist/bridge/SandboxBridge.d.ts +4 -1
  2. package/dist/bridge/SandboxBridge.d.ts.map +1 -1
  3. package/dist/bridge/SandboxBridge.js +95 -57
  4. package/dist/bridge/dataSources/query.d.ts +26 -0
  5. package/dist/bridge/dataSources/query.d.ts.map +1 -0
  6. package/dist/bridge/dataSources/query.js +377 -0
  7. package/dist/bridge/hostState.d.ts +4 -3
  8. package/dist/bridge/hostState.d.ts.map +1 -1
  9. package/dist/bridge/hostState.js +7 -3
  10. package/dist/bridge/sandboxClient.d.ts +4 -2
  11. package/dist/bridge/sandboxClient.d.ts.map +1 -1
  12. package/dist/bridge/sandboxClient.js +10 -4
  13. package/dist/protocol/messages/init.d.ts +1 -1
  14. package/dist/protocol/messages/init.d.ts.map +1 -1
  15. package/dist/protocol/messages/initResult.d.ts +8 -1
  16. package/dist/protocol/messages/initResult.d.ts.map +1 -1
  17. package/dist/protocol/messages/queryDataSource.d.ts +955 -0
  18. package/dist/protocol/messages/queryDataSource.d.ts.map +1 -1
  19. package/dist/protocol/messages/queryDataSource.js +100 -0
  20. package/dist/protocol/messages/queryDataSourceResult.d.ts +1 -1
  21. package/dist/protocol/messages/queryDataSourceResult.d.ts.map +1 -1
  22. package/dist/protocol/messages/sandboxToHost.d.ts +349 -0
  23. package/dist/protocol/messages/sandboxToHost.d.ts.map +1 -1
  24. package/dist/react/useDataSource.d.ts.map +1 -1
  25. package/dist/react/useDataSource.js +10 -5
  26. package/dist/types.d.ts +45 -0
  27. package/dist/types.d.ts.map +1 -1
  28. package/dist/version.js +1 -1
  29. package/docs/data-sources.md +39 -3
  30. package/docs/errors.md +16 -0
  31. package/docs/lifecycle.md +74 -20
  32. package/package.json +1 -1
  33. package/src/bridge/SandboxBridge.ts +112 -64
  34. package/src/bridge/dataSources/query.ts +470 -0
  35. package/src/bridge/hostState.ts +11 -3
  36. package/src/bridge/sandboxClient.ts +20 -4
  37. package/src/react/useDataSource.ts +16 -5
  38. package/src/types.ts +47 -0
@@ -41,7 +41,10 @@ Use `row.update(...)` whenever you already have a row in hand. For pages you don
41
41
  ```ts
42
42
  function useDataSource(
43
43
  key: string,
44
- options?: { limit?: number },
44
+ options?: {
45
+ limit?: number;
46
+ filter?: NotionDataSourceFilter;
47
+ },
45
48
  ): UseDataSourceResult;
46
49
 
47
50
  type UseDataSourceResult = {
@@ -56,7 +59,36 @@ type UseDataSourceResult = {
56
59
  };
57
60
  ```
58
61
 
59
- Reads the data source mapped to `key`. `limit` defaults to 20 and is capped at 999. To show more rows, keep the desired limit in your own component state and pass the larger value back into `useDataSource(key, { limit })`. `propertyIdsByKey` maps manifest property keys to raw Notion property IDs; unbound keys map to `undefined`. `propertySchemasByKey` exposes the corresponding schemas and is likewise `undefined` for declared-but-unbound slots.
62
+ Reads the data source mapped to `key`. The SDK uses a default `limit` of 20 and caps it at 999. To request more rows, store the limit in component state and pass it to `useDataSource` again. Each request is bounded. The hook does not provide cursor pagination.
63
+
64
+ `filter` accepts one property condition or one shallow `and` group. Use `key` for a semantic property key. Use `propertyId` for a raw Notion property ID. The SDK resolves semantic keys before it sends the bridge request. The SDK reports local errors for unknown or unbound keys, property-type mismatches, unsupported filter fields, invalid operators, and invalid values.
65
+
66
+ ```tsx
67
+ const query = useDataSource("tasks", {
68
+ filter: {
69
+ and: [
70
+ { key: "title", title: { contains: "launch" } },
71
+ { key: "done", checkbox: { equals: false } },
72
+ ],
73
+ },
74
+ limit: 50,
75
+ });
76
+ ```
77
+
78
+ Filters support these property types:
79
+
80
+ - Text properties: `title`, `rich_text`, `url`, `email`, and `phone_number`. They support `equals`, `does_not_equal`, `contains`, `does_not_contain`, `starts_with`, `ends_with`, `is_empty`, and `is_not_empty`.
81
+ - `number`: `equals`, `does_not_equal`, `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to`, `is_empty`, and `is_not_empty`.
82
+ - `checkbox`: `equals` and `does_not_equal`.
83
+ - `select` and `status`: `equals`, `does_not_equal`, `is_empty`, and `is_not_empty`.
84
+ - `multi_select`: `contains`, `contains_all`, `does_not_contain`, `is_empty`, and `is_not_empty`.
85
+ - `date`: `equals`, `before`, `after`, `on_or_before`, `on_or_after`, `is_empty`, and `is_not_empty`.
86
+
87
+ Select, multi-select, and status values can be one option name or an array of option names. For multi-select properties, `contains` matches any supplied option and `contains_all` requires every supplied option. Empty checks use `{ is_empty: true }` or `{ is_not_empty: true }`. Date comparisons accept ISO dates or ISO timestamps.
88
+
89
+ The `and` group can contain up to 25 conditions. An empty group is valid. Nested groups and `or` groups are not supported.
90
+
91
+ `propertyIdsByKey` maps manifest property keys to raw Notion property IDs; unbound keys map to `undefined`. `propertySchemasByKey` exposes the corresponding schemas and is likewise `undefined` for declared-but-unbound slots.
60
92
 
61
93
  Query failures follow the SDK's [error-handling contract](./errors.md).
62
94
 
@@ -161,7 +193,11 @@ export function ScoreList() {
161
193
  - `NotionDataSourcePageUpdateArgs` / `UpdatePageResult` — arguments and result for the per-page `update` helper.
162
194
  - `NotionDataSourcePageUpdateInput` — deprecated alias for `NotionDataSourcePageUpdateArgs`.
163
195
  - `NotionDataSourcePageUpdateResult` — deprecated alias for `UpdatePageResult`.
164
- - `UseDataSourceOptions` — options accepted by `useDataSource`, currently `{ limit?: number }`.
196
+ - `UseDataSourceOptions` — options accepted by `useDataSource`: `limit?: number` and `filter?: NotionDataSourceFilter`.
197
+ - `NotionDataSourceFilter` — one property condition or one shallow `and` group.
198
+ - `NotionDataSourcePropertyFilter` — one property address combined with a type-specific operator.
199
+ - `NotionDataSourcePropertyAddress` — either a semantic property `key` or a raw `propertyId`.
200
+ - `NotionDataSourceTextFilterOperator`, `NotionDataSourceNumberFilterOperator`, `NotionDataSourceCheckboxFilterOperator`, `NotionDataSourceOptionFilterOperator`, `NotionDataSourceContainsFilterOperator`, and `NotionDataSourceDateFilterOperator` — the operators accepted for each property type.
165
201
 
166
202
  ### Property schemas
167
203
 
package/docs/errors.md CHANGED
@@ -39,6 +39,22 @@ Unlike request helpers, `initCustomBlock` rejects with `CustomBlockInitializatio
39
39
 
40
40
  `CustomBlockInitializationErrorInfo` is the matching `{ code, message, isRetryable }` object type.
41
41
 
42
+ Initialization errors have direction-specific meanings:
43
+
44
+ - `invalid_connect_payload` means that the host could not parse the SDK's
45
+ `connect` message. The host sends this code in `init.error` when it can read
46
+ the `initializationId`.
47
+ - `invalid_init_payload` means that the SDK could not parse the host's `init`
48
+ message. The SDK sends this code in `initResult.error` when it can read the
49
+ expected `initializationId`.
50
+ - `invalid_init_bindings` means that the SDK could not apply the data source
51
+ bindings in the host's valid `init` message. The SDK sends this code in
52
+ `initResult.error`.
53
+ - `invalid_init_result_payload` means that the host could not parse the SDK's
54
+ `initResult`. This is a host-local error. The SDK does not send this code.
55
+ - `no_init_result` means that the host did not receive a valid `initResult`
56
+ before its timeout.
57
+
42
58
  See [Lifecycle and initialization](./lifecycle.md) for initialization-specific error codes.
43
59
 
44
60
  ## Handling errors
package/docs/lifecycle.md CHANGED
@@ -1,27 +1,65 @@
1
1
  # Lifecycle
2
2
 
3
- The SDK host handshake, the React wrapper that runs it, and the auto-resize hook that keeps the iframe in sync with your content.
4
-
5
- ## Handshake
6
-
7
- `initCustomBlock()` handles three messages:
8
-
9
- 1. The SDK sends `connect` with a new initialization ID and version information.
10
- 2. The host sends `init`. It includes the same ID, the manifest, block context, current user, and data source bindings.
11
- 3. The SDK sends `initResult` with the ID and the initial block height.
12
-
13
- The promise resolves when the SDK applies the host state. Await it before React renders so hooks read initialized state. The SDK sends `initResult` after the initial render.
14
-
15
- - Rejects with `CustomBlockInitializationError` code `init_timeout` if the host doesn't respond.
16
- - In a top-level browser tab (no parent frame), rejects with `NotInIframeError` code `not_in_iframe`. `<NotionCustomBlock>` catches this, seeds placeholders, and renders `children` behind a warning banner so dev-time previews still work.
17
- - After init, compatible hosts use `*Changed` events (e.g. `themeChanged`, `contrastModeChanged`, `parentChanged`, `dataSourcesChanged`) to push updates and re-render the relevant hooks.
18
- - `initCustomBlock` is idempotent; subsequent calls return the same promise.
3
+ The SDK and host use a three-message `postMessage` handshake to initialize
4
+ a custom block.
5
+
6
+ This handshake can be completed with one of these APIs:
7
+
8
+ - `<NotionCustomBlock>` for most React blocks. It starts initialization and
9
+ renders `children` after the host state is ready.
10
+ - `useCustomBlockInit()` for React blocks that need to control loading and
11
+ error states.
12
+ - `initCustomBlock()` for framework-neutral code or code that must await
13
+ initialization before it renders.
14
+
15
+ Custom bridge integrations can also handle the `postMessage` wire messages
16
+ directly. This is advanced usage for host or integration authors.
17
+
18
+ ## Initialization protocol message
19
+
20
+ The full `postMessage` handshake is:
21
+
22
+ 1. The SDK sends `connect` with a new `initializationId` and version
23
+ information.
24
+ 2. The host sends `init` with the same `initializationId`, the manifest, data source bindings,
25
+ and additional block and app context.
26
+ 3. The SDK validates and applies `init`, renders the initial content, and sends
27
+ `initResult` with the same `initializationId` and the initial block height.
28
+
29
+ Both sides must check that the `initializationId` matches. The SDK ignores an
30
+ `init` with a different ID. It sends `initResult` only after it applies a
31
+ matching `init`.
32
+
33
+ The initialization promise resolves after the SDK applies the host state.
34
+ Await it before rendering code that reads initialized state. The SDK sends
35
+ `initResult` after the initial render.
36
+
37
+ - The promise rejects with `CustomBlockInitializationError` code
38
+ `init_timeout` if the host does not send a usable `init`.
39
+ - If a matching `init` is malformed, the SDK sends `initResult.error` with code
40
+ `invalid_init_payload` and rejects with the same structured error.
41
+ - If the host sends incomplete or invalid data source bindings, the SDK sends
42
+ `initResult.error` with code `invalid_init_bindings` and rejects with the same
43
+ structured error.
44
+ - If the SDK cannot correlate a malformed `init` to the handshake, it sends
45
+ `invalidHostMessage` when safe.
46
+ - After initialization, compatible hosts use `*Changed` messages to push live
47
+ updates.
48
+ - `initCustomBlock()` is idempotent. Later calls return the same promise.
49
+
50
+ ## Rendering inside a host
51
+
52
+ The SDK expects the custom block to run inside an iframe. In a top-level
53
+ browser tab, initialization rejects with `NotInIframeError` code
54
+ `not_in_iframe`.
55
+
56
+ `<NotionCustomBlock>` catches this error and renders a standalone preview with
57
+ a warning. This supports local development when no host is available.
19
58
 
20
59
  ## Sizing
21
60
 
22
- All blocks resize to fit their content. The Notion app limits block height to 10,000 pixels.
23
-
24
- To set a shorter height, set `max-height` and `overflow-y` on `#root`:
61
+ Custom blocks resize to fit their content by default. To set a shorter height, set
62
+ `max-height` and `overflow-y` on `#root`:
25
63
 
26
64
  ```css
27
65
  #root {
@@ -30,9 +68,25 @@ To set a shorter height, set `max-height` and `overflow-y` on `#root`:
30
68
  }
31
69
  ```
32
70
 
71
+ The Notion app enforces a maximum custom block height of 10,000 pixels.
72
+
73
+ The SDK measures the `#root` element and sends its initial height in
74
+ `initResult.success`. It sends later height changes in `resize` messages. The
75
+ host uses these messages to keep the iframe height in sync.
76
+
77
+ Using `<NotionCustomBlock>` enables this behavior automatically. Use
78
+ `customBlock.autoResize()` for a custom, framework-neutral initialization wrapper.
79
+
33
80
  ## API
34
81
 
35
- Import framework-neutral APIs from `@notionhq/custom-blocks`; import React hooks and components from `@notionhq/custom-blocks/react`. The runtime APIs below assume `initCustomBlock()` has resolved — initialized-only hooks and `customBlock` getters throw if called before that. Inside `<NotionCustomBlock>` (or past the `isLoaded` gate of `useCustomBlockInit`), single-value hooks return non-nullable values.
82
+ Import framework-neutral APIs from `@notionhq/custom-blocks`. Import React
83
+ hooks and components from `@notionhq/custom-blocks/react`.
84
+
85
+ The APIs below require successful initialization unless stated otherwise.
86
+ Initialized-only hooks and `customBlock` getters throw before
87
+ `initCustomBlock()` resolves. Inside `<NotionCustomBlock>`, or after the
88
+ `isLoaded` gate of `useCustomBlockInit`, single-value hooks return non-nullable
89
+ values.
36
90
 
37
91
  ### `<NotionCustomBlock>`
38
92
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/custom-blocks",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -52,6 +52,7 @@ import {
52
52
  NOTION_DARK_BACKGROUND_BASE,
53
53
  NOTION_LIGHT_BACKGROUND_BASE,
54
54
  } from "./appearance.js"
55
+ import { resolveDataSourceQuery } from "./dataSources/query.js"
55
56
  import { resolveDataSources } from "./dataSources/resolve.js"
56
57
  import { resolvePropertyWriteMapForDataSource } from "./dataSources/resolveProperty.js"
57
58
  import {
@@ -258,6 +259,16 @@ export class SandboxBridge {
258
259
  parsed.issues,
259
260
  )
260
261
  const incomingType = readIncomingType(event.data)
262
+ if (
263
+ !this.hasReceivedInit &&
264
+ incomingType === "init" &&
265
+ this.initializationId !== undefined &&
266
+ readInitializationId(event.data) === this.initializationId
267
+ ) {
268
+ this.hasReceivedInit = true
269
+ this.sendInitResultError(invalidInitPayloadError(parsed.issues))
270
+ return
271
+ }
261
272
  // Loop guard: never NACK a NACK. Excludes both directions —
262
273
  // `invalidSandboxMessage` is what the host normally sends, but
263
274
  // a buggy host that echoes our own `invalidHostMessage` back
@@ -383,12 +394,14 @@ export class SandboxBridge {
383
394
  nextBindings,
384
395
  })
385
396
  this.latestDataSourceBindings = nextBindings
386
- // Drop cached query state for keys that no longer exist in the mapping.
397
+ // Drop cached query state for subscriptions whose key no longer exists.
387
398
  const nextKeys = new Set(dataSources.map(s => s.key))
388
399
  const prunedState: Record<string, DataSourceQueryState> = {}
389
- for (const [key, state] of Object.entries(hostState.dataSourceState)) {
390
- if (nextKeys.has(key)) {
391
- prunedState[key] = state
400
+ for (const [subscriptionId, state] of Object.entries(
401
+ hostState.dataSourceState,
402
+ )) {
403
+ if (nextKeys.has(state.dataSourceKey)) {
404
+ prunedState[subscriptionId] = state
392
405
  }
393
406
  }
394
407
  this.hostState = {
@@ -466,13 +479,10 @@ export class SandboxBridge {
466
479
  }
467
480
 
468
481
  case "queryDataSourceResult": {
469
- const queryEntry = Object.entries(hostState.dataSourceState).find(
470
- ([, state]) => state.subscriptionId === message.subscriptionId,
471
- )
472
- if (queryEntry === undefined) {
482
+ const currentState = hostState.dataSourceState[message.subscriptionId]
483
+ if (currentState === undefined) {
473
484
  return
474
485
  }
475
- const [key, currentState] = queryEntry
476
486
  const queryResult =
477
487
  message.status === "error"
478
488
  ? {
@@ -489,13 +499,14 @@ export class SandboxBridge {
489
499
  ...hostState,
490
500
  dataSourceState: {
491
501
  ...hostState.dataSourceState,
492
- [key]: {
502
+ [message.subscriptionId]: {
503
+ dataSourceKey: currentState.dataSourceKey,
493
504
  items: queryResult.items,
494
505
  isLoading: false,
495
506
  hasMore: queryResult.hasMore,
496
507
  error: queryResult.error,
497
- subscriptionId: message.subscriptionId,
498
508
  latestLimit: currentState.latestLimit,
509
+ latestQueryIdentity: currentState.latestQueryIdentity,
499
510
  },
500
511
  },
501
512
  }
@@ -548,6 +559,25 @@ export class SandboxBridge {
548
559
  this.flushPendingOutboundMessages()
549
560
  }
550
561
 
562
+ private sendInitResultError(error: CustomBlockInitResultErrorInfo) {
563
+ if (this.hasSentInitResult || this.initializationId === undefined) {
564
+ return
565
+ }
566
+ const result: InitResultMessage = {
567
+ type: "initResult",
568
+ initializationId: this.initializationId,
569
+ status: "error",
570
+ error,
571
+ }
572
+ this.hasSentInitResult = true
573
+ this.postToHost(result)
574
+ if (this.rejectInit) {
575
+ this.rejectInit(new CustomBlockInitializationError(error))
576
+ this.resolveInit = undefined
577
+ this.rejectInit = undefined
578
+ }
579
+ }
580
+
551
581
  private applyInit(message: InitMessage, postResult: boolean) {
552
582
  if (postResult) {
553
583
  this.isMockState = false
@@ -581,18 +611,7 @@ export class SandboxBridge {
581
611
  : resolveMockDataSources(this.latestDataSourceBindings)
582
612
  const bindingError = getInitBindingError(message.manifest, dataSources)
583
613
  if (postResult && bindingError !== undefined) {
584
- const result: InitResultMessage = {
585
- type: "initResult",
586
- initializationId: message.initializationId,
587
- status: "error",
588
- error: bindingError,
589
- }
590
- this.postToHost(result)
591
- if (this.rejectInit) {
592
- this.rejectInit(new CustomBlockInitializationError(bindingError))
593
- this.resolveInit = undefined
594
- this.rejectInit = undefined
595
- }
614
+ this.sendInitResultError(bindingError)
596
615
  return
597
616
  }
598
617
  this.hostState = {
@@ -654,7 +673,15 @@ export class SandboxBridge {
654
673
  }
655
674
  }
656
675
 
657
- queryDataSource(key: string, options: UseDataSourceOptions = {}) {
676
+ createDataSourceSubscriptionId(): string {
677
+ return `data-source:${globalThis.crypto.randomUUID()}`
678
+ }
679
+
680
+ queryDataSource(
681
+ subscriptionId: string,
682
+ key: string,
683
+ options: UseDataSourceOptions = {},
684
+ ) {
658
685
  if (this.hostState.status !== "initialized") {
659
686
  return
660
687
  }
@@ -662,10 +689,13 @@ export class SandboxBridge {
662
689
  const dataSource = this.hostState.dataSources.find(
663
690
  entry => entry.key === key,
664
691
  )
692
+ const subscriptionState = this.hostState.dataSourceState[subscriptionId]
665
693
  const currentState =
666
- this.hostState.dataSourceState[key] ?? createEmptyDataSourceQueryState()
694
+ subscriptionState?.dataSourceKey === key
695
+ ? subscriptionState
696
+ : createEmptyDataSourceQueryState(key)
667
697
  if (dataSource === undefined) {
668
- this.setDataSourceQueryError(key, currentState, {
698
+ this.setDataSourceQueryError(subscriptionId, currentState, {
669
699
  code: "unknown_data_source_key",
670
700
  message: `Unknown data source key "${key}". Known keys: [${this.hostState.dataSources.map(entry => entry.key).join(", ")}].`,
671
701
  isRetryable: false,
@@ -673,7 +703,7 @@ export class SandboxBridge {
673
703
  return
674
704
  }
675
705
  if (dataSource.collectionPointer === undefined) {
676
- this.setDataSourceQueryError(key, currentState, {
706
+ this.setDataSourceQueryError(subscriptionId, currentState, {
677
707
  code: "unmapped_data_source",
678
708
  message: `Data source "${key}" has not been mapped to a database yet.`,
679
709
  isRetryable: false,
@@ -681,15 +711,24 @@ export class SandboxBridge {
681
711
  return
682
712
  }
683
713
 
684
- const limit = resolveDataSourceQueryLimit(options.limit)
685
- const subscriptionId = makeDataSourceSubscriptionId({
714
+ const resolvedQuery = resolveDataSourceQuery({
715
+ dataSources: this.hostState.dataSources,
686
716
  key,
717
+ options,
687
718
  })
719
+ if (resolvedQuery.status === "error") {
720
+ this.setDataSourceQueryError(subscriptionId, currentState, {
721
+ code: "invalid_data_source_query",
722
+ message: resolvedQuery.error,
723
+ isRetryable: false,
724
+ })
725
+ return
726
+ }
727
+ const query = resolvedQuery.query
688
728
 
689
729
  if (
690
730
  currentState.isLoading &&
691
- currentState.subscriptionId === subscriptionId &&
692
- currentState.latestLimit === limit
731
+ currentState.latestQueryIdentity === query.identity
693
732
  ) {
694
733
  return
695
734
  }
@@ -698,12 +737,13 @@ export class SandboxBridge {
698
737
  ...this.hostState,
699
738
  dataSourceState: {
700
739
  ...this.hostState.dataSourceState,
701
- [key]: {
740
+ [subscriptionId]: {
702
741
  ...currentState,
742
+ dataSourceKey: key,
703
743
  isLoading: true,
704
744
  error: undefined,
705
- subscriptionId,
706
- latestLimit: limit,
745
+ latestLimit: query.limit,
746
+ latestQueryIdentity: query.identity,
707
747
  },
708
748
  },
709
749
  }
@@ -712,14 +752,15 @@ export class SandboxBridge {
712
752
  const outbound: QueryDataSourceMessage = {
713
753
  type: "queryDataSource",
714
754
  subscriptionId,
715
- dataSourceId: dataSource.collectionPointer.id,
716
- limit,
755
+ dataSourceId: query.dataSourceId,
756
+ limit: query.limit,
757
+ ...(query.filter !== undefined ? { filter: query.filter } : {}),
717
758
  }
718
759
  this.postToHost(outbound)
719
760
  }
720
761
 
721
762
  private setDataSourceQueryError(
722
- key: string,
763
+ subscriptionId: string,
723
764
  currentState: DataSourceQueryState,
724
765
  error: CustomBlockQueryDataSourceErrorInfo,
725
766
  ) {
@@ -730,18 +771,33 @@ export class SandboxBridge {
730
771
  ...this.hostState,
731
772
  dataSourceState: {
732
773
  ...this.hostState.dataSourceState,
733
- [key]: {
774
+ [subscriptionId]: {
734
775
  ...currentState,
735
776
  isLoading: false,
736
777
  error,
737
- subscriptionId: undefined,
738
778
  latestLimit: undefined,
779
+ latestQueryIdentity: undefined,
739
780
  },
740
781
  },
741
782
  }
742
783
  this.notify()
743
784
  }
744
785
 
786
+ releaseDataSourceSubscription(subscriptionId: string) {
787
+ if (
788
+ this.hostState.status !== "initialized" ||
789
+ this.hostState.dataSourceState[subscriptionId] === undefined
790
+ ) {
791
+ return
792
+ }
793
+ const { [subscriptionId]: _, ...dataSourceState } =
794
+ this.hostState.dataSourceState
795
+ this.hostState = {
796
+ ...this.hostState,
797
+ dataSourceState,
798
+ }
799
+ }
800
+
745
801
  postResize(height: number) {
746
802
  if (typeof window === "undefined") {
747
803
  return
@@ -1036,34 +1092,26 @@ function reuseDataSourcesForUnchangedBindings(args: {
1036
1092
  })
1037
1093
  }
1038
1094
 
1039
- // The default number of items to return in a live snapshot response if no limit is provided.
1040
- const DEFAULT_DATA_SOURCE_QUERY_LIMIT = 20
1041
-
1042
- // The maximum number of items to return in a single live snapshot response.
1043
- const MAX_DATA_SOURCE_QUERY_LIMIT = 999
1044
-
1045
- function resolveDataSourceQueryLimit(limit: number | undefined): number {
1046
- if (limit === undefined) {
1047
- return DEFAULT_DATA_SOURCE_QUERY_LIMIT
1048
- }
1049
- if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1) {
1050
- console.warn(
1051
- `[custom-blocks-sdk] useDataSource limit must be a positive integer; using ${DEFAULT_DATA_SOURCE_QUERY_LIMIT}.`,
1052
- )
1053
- return DEFAULT_DATA_SOURCE_QUERY_LIMIT
1054
- }
1055
- if (limit > MAX_DATA_SOURCE_QUERY_LIMIT) {
1056
- console.warn(
1057
- `[custom-blocks-sdk] useDataSource limit is capped at ${MAX_DATA_SOURCE_QUERY_LIMIT}.`,
1058
- )
1059
- return MAX_DATA_SOURCE_QUERY_LIMIT
1095
+ function invalidInitPayloadError(
1096
+ issues: readonly v.BaseIssue<unknown>[],
1097
+ ): CustomBlockInitResultErrorInfo {
1098
+ return {
1099
+ code: "invalid_init_payload",
1100
+ message: formatInvalidHostReason("init", issues),
1101
+ isRetryable: false,
1060
1102
  }
1061
- return limit
1062
1103
  }
1063
1104
 
1064
- function makeDataSourceSubscriptionId(args: { key: string }): string {
1065
- const { key } = args
1066
- return `data-source:${encodeURIComponent(key)}`
1105
+ function readInitializationId(data: unknown): string | undefined {
1106
+ if (
1107
+ typeof data === "object" &&
1108
+ data !== null &&
1109
+ "initializationId" in data &&
1110
+ typeof data.initializationId === "string"
1111
+ ) {
1112
+ return data.initializationId
1113
+ }
1114
+ return undefined
1067
1115
  }
1068
1116
 
1069
1117
  function getInitBindingError(