@sanity/sdk 3.0.0-rc.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +12 -0
  2. package/dist/_exports/_internal.d.ts +71 -3
  3. package/dist/_exports/_internal.d.ts.map +1 -1
  4. package/dist/_exports/_internal.js +332 -24
  5. package/dist/_exports/_internal.js.map +1 -1
  6. package/dist/_exports/agent.d.ts +65 -0
  7. package/dist/_exports/agent.d.ts.map +1 -0
  8. package/dist/_exports/agent.js +71 -0
  9. package/dist/_exports/agent.js.map +1 -0
  10. package/dist/_exports/comlink.d.ts +118 -0
  11. package/dist/_exports/comlink.d.ts.map +1 -0
  12. package/dist/_exports/comlink.js +69 -0
  13. package/dist/_exports/comlink.js.map +1 -0
  14. package/dist/authStore-C0o1NOxW.d.ts +3165 -0
  15. package/dist/authStore-C0o1NOxW.d.ts.map +1 -0
  16. package/dist/authStore-CtE9bLHW.js +754 -0
  17. package/dist/authStore-CtE9bLHW.js.map +1 -0
  18. package/dist/clientStore-BtE2-q2i.js +90 -0
  19. package/dist/clientStore-BtE2-q2i.js.map +1 -0
  20. package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
  21. package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
  22. package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
  23. package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
  24. package/dist/getEnv-D5RGkQLu.js +8 -0
  25. package/dist/getEnv-D5RGkQLu.js.map +1 -0
  26. package/dist/getNodeState-CR3__B2D.js +67 -0
  27. package/dist/getNodeState-CR3__B2D.js.map +1 -0
  28. package/dist/index.d.ts +3 -2335
  29. package/dist/index.js +4048 -3878
  30. package/dist/index.js.map +1 -1
  31. package/dist/logger-DAtJb5RQ.js +173 -0
  32. package/dist/logger-DAtJb5RQ.js.map +1 -0
  33. package/dist/object-CxXlSVg7.js +83 -0
  34. package/dist/object-CxXlSVg7.js.map +1 -0
  35. package/dist/queryStore-C7yMHPUI.js +1346 -0
  36. package/dist/queryStore-C7yMHPUI.js.map +1 -0
  37. package/dist/rolldown-runtime-vyAXikos.js +9 -0
  38. package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
  39. package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
  40. package/dist/telemetryManager-UIEiMtC0.js +127 -0
  41. package/dist/telemetryManager-UIEiMtC0.js.map +1 -0
  42. package/dist/version-D8RP_Q59.js +9 -0
  43. package/dist/version-D8RP_Q59.js.map +1 -0
  44. package/package.json +11 -17
  45. package/src/_exports/_internal.ts +1 -0
  46. package/src/_exports/agent.ts +19 -0
  47. package/src/_exports/comlink.ts +16 -0
  48. package/src/_exports/index.ts +45 -90
  49. package/src/applications/applications.test-d.ts +5 -0
  50. package/src/applications/applications.ts +5 -2
  51. package/src/auth/authMode.test.ts +0 -25
  52. package/src/auth/authMode.ts +3 -6
  53. package/src/auth/authStore.test.ts +1 -1
  54. package/src/auth/authStore.ts +0 -1
  55. package/src/comments/addonDatasetStore.test.ts +215 -0
  56. package/src/comments/addonDatasetStore.ts +249 -0
  57. package/src/comments/buildCommentThreads.test.ts +83 -0
  58. package/src/comments/buildCommentThreads.ts +54 -0
  59. package/src/comments/commentActions.test.ts +540 -0
  60. package/src/comments/commentActions.ts +555 -0
  61. package/src/comments/commentFieldPath.test.ts +40 -0
  62. package/src/comments/commentFieldPath.ts +44 -0
  63. package/src/comments/commentsConstants.ts +80 -0
  64. package/src/comments/commentsStore.test.ts +419 -0
  65. package/src/comments/commentsStore.ts +483 -0
  66. package/src/comments/normalizeComment.test.ts +116 -0
  67. package/src/comments/normalizeComment.ts +45 -0
  68. package/src/comments/observeComments.test.ts +193 -0
  69. package/src/comments/observeComments.ts +113 -0
  70. package/src/comments/reducers.test.ts +277 -0
  71. package/src/comments/reducers.ts +316 -0
  72. package/src/comments/types.ts +249 -0
  73. package/src/comments/weakenReferences.test.ts +47 -0
  74. package/src/comments/weakenReferences.ts +29 -0
  75. package/src/config/sanityConfig.ts +3 -75
  76. package/src/datasets/datasets.test.ts +44 -9
  77. package/src/datasets/datasets.ts +24 -23
  78. package/src/document/applyDocumentActions.test.ts +8 -12
  79. package/src/document/documentConstants.ts +17 -0
  80. package/src/document/documentStore.test.ts +173 -19
  81. package/src/document/documentStore.ts +36 -5
  82. package/src/document/patchOperations.test.ts +27 -0
  83. package/src/document/patchOperations.ts +10 -1
  84. package/src/document/permissions.test.ts +3 -1
  85. package/src/document/reducers.test.ts +357 -0
  86. package/src/document/reducers.ts +125 -27
  87. package/src/document/util.test.ts +46 -0
  88. package/src/document/util.ts +37 -1
  89. package/src/favorites/favorites.test.ts +64 -24
  90. package/src/favorites/favorites.ts +73 -22
  91. package/src/installations/installations.test-d.ts +5 -0
  92. package/src/installations/installations.ts +4 -0
  93. package/src/presence/bifurTransport.test.ts +155 -3
  94. package/src/presence/bifurTransport.ts +79 -12
  95. package/src/presence/paths.test.ts +70 -0
  96. package/src/presence/paths.ts +41 -0
  97. package/src/presence/presenceStore.test.ts +822 -2
  98. package/src/presence/presenceStore.ts +518 -102
  99. package/src/presence/types.ts +175 -3
  100. package/src/preview/types.ts +0 -25
  101. package/src/projection/projectionStore.test.ts +0 -8
  102. package/src/projection/types.ts +0 -8
  103. package/src/query/queryStore.ts +7 -6
  104. package/src/store/createActionBinder.test.ts +51 -29
  105. package/src/store/createActionBinder.ts +16 -62
  106. package/src/store/createSanityInstance.test.ts +0 -68
  107. package/src/store/createSanityInstance.ts +0 -63
  108. package/src/store/createStateSourceAction.ts +1 -1
  109. package/src/store/fetcherStore.test.ts +23 -1
  110. package/src/store/fetcherStore.ts +3 -3
  111. package/src/users/reducers.test.ts +0 -3
  112. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +0 -1147
  113. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +0 -1
  114. package/dist/_chunks-es/_internal.js +0 -134
  115. package/dist/_chunks-es/_internal.js.map +0 -1
  116. package/dist/_chunks-es/createGroqSearchFilter.js +0 -1864
  117. package/dist/_chunks-es/createGroqSearchFilter.js.map +0 -1
  118. package/dist/_chunks-es/telemetryManager.js +0 -93
  119. package/dist/_chunks-es/telemetryManager.js.map +0 -1
  120. package/dist/index.d.ts.map +0 -1
  121. package/src/applications/userApplications.test.ts +0 -126
  122. package/src/applications/userApplications.ts +0 -261
  123. package/src/preview/getPreviewState.test.ts +0 -137
  124. package/src/preview/getPreviewState.ts +0 -69
  125. package/src/preview/resolvePreview.test.ts +0 -64
  126. package/src/preview/resolvePreview.ts +0 -43
  127. package/src/utils/createFetcherStore.test.ts +0 -171
  128. package/src/utils/createFetcherStore.ts +0 -284
@@ -1,7 +1,7 @@
1
1
  import {type BifurClient, fromUrl} from '@sanity/bifur-client'
2
2
  import {type SanityClient} from '@sanity/client'
3
- import {EMPTY, fromEvent, type Observable} from 'rxjs'
4
- import {map, share, switchMap} from 'rxjs/operators'
3
+ import {defer, EMPTY, fromEvent, merge, type Observable, timer} from 'rxjs'
4
+ import {catchError, distinctUntilChanged, map, retry, share, switchMap} from 'rxjs/operators'
5
5
 
6
6
  import {
7
7
  type BifurTransportOptions,
@@ -11,6 +11,12 @@ import {
11
11
  type TransportMessage,
12
12
  } from './types'
13
13
 
14
+ /**
15
+ * Longest gap between reconnection attempts, matching the Studio's presence
16
+ * transport so both behave the same way on a flaky connection.
17
+ */
18
+ const CONNECT_RETRY_MAX_DELAY = 1000 * 240
19
+
14
20
  type BifurStateMessage = {
15
21
  type: 'state'
16
22
  i: string
@@ -91,13 +97,80 @@ const handleIncomingMessage = (event: IncomingBifurEvent): TransportEvent => {
91
97
  }
92
98
  }
93
99
 
100
+ /**
101
+ * Emits an incrementing generation number each time the socket becomes live.
102
+ *
103
+ * `@sanity/bifur-client` does not reconnect on its own — it errors the stream
104
+ * when the socket closes. Without the `retry` below, presence stops for the
105
+ * life of the page after the first dropped connection.
106
+ */
107
+ const createConnections = (bifur: BifurClient): Observable<number> => {
108
+ let generation = 0
109
+
110
+ return defer(() => {
111
+ const current = ++generation
112
+ // `heartbeats` emits as soon as the socket is open and authorized, then on
113
+ // every server heartbeat and every response. Subscribing to it is also what
114
+ // keeps the socket alive between requests.
115
+ return bifur.heartbeats.pipe(map(() => current))
116
+ }).pipe(
117
+ retry({
118
+ delay: (_error, retryCount) =>
119
+ timer(Math.min(CONNECT_RETRY_MAX_DELAY, 2 ** retryCount * 100)),
120
+ resetOnSuccess: true,
121
+ }),
122
+ // After `retry` so that one operator instance spans reconnects, making each
123
+ // live socket yield exactly one emission.
124
+ distinctUntilChanged(),
125
+ share(),
126
+ )
127
+ }
128
+
129
+ /**
130
+ * Presence events from everyone else in the room, re-established on every live
131
+ * connection.
132
+ *
133
+ * A socket close errors this stream just as it errors the heartbeats, so without
134
+ * re-subscribing the client would keep announcing itself after a reconnect while
135
+ * never hearing from anyone again. Driving it from `connections$` rather than
136
+ * giving it its own `retry` keeps a single reconnect loop, instead of two
137
+ * independent backoffs racing over one refcounted socket.
138
+ */
139
+ const createIncomingEvents = (
140
+ bifur: BifurClient,
141
+ connections$: Observable<number>,
142
+ ): Observable<TransportEvent> =>
143
+ connections$.pipe(
144
+ switchMap(() =>
145
+ bifur.listen<IncomingBifurEvent>('presence').pipe(
146
+ // Let a dead listener go quietly. The next connection replaces it, and
147
+ // surfacing the error here would tear down the reconnect loop with it.
148
+ catchError(() => EMPTY),
149
+ ),
150
+ ),
151
+ map(handleIncomingMessage),
152
+ share(),
153
+ )
154
+
155
+ /**
156
+ * Emits when the page is going away. `pagehide` is needed alongside
157
+ * `beforeunload` because iOS Safari and pages entering the back/forward cache
158
+ * never fire `beforeunload`.
159
+ */
160
+ const createUnload = (): Observable<void> => {
161
+ if (typeof window === 'undefined') return EMPTY
162
+
163
+ return merge(fromEvent(window, 'beforeunload'), fromEvent(window, 'pagehide')).pipe(
164
+ map(() => undefined),
165
+ )
166
+ }
167
+
94
168
  export const createBifurTransport = (options: BifurTransportOptions): PresenceTransport => {
95
169
  const {client, token$, sessionId} = options
96
170
  const bifur = getBifurClient(client, token$)
97
171
 
98
- const incomingEvents$: Observable<TransportEvent> = bifur
99
- .listen<IncomingBifurEvent>('presence')
100
- .pipe(map(handleIncomingMessage))
172
+ const connections$ = createConnections(bifur)
173
+ const incomingEvents$ = createIncomingEvents(bifur, connections$)
101
174
 
102
175
  const dispatchMessage = (message: TransportMessage): Observable<void> => {
103
176
  switch (message.type) {
@@ -115,11 +188,5 @@ export const createBifurTransport = (options: BifurTransportOptions): PresenceTr
115
188
  }
116
189
  }
117
190
 
118
- if (typeof window !== 'undefined') {
119
- fromEvent(window, 'beforeunload')
120
- .pipe(switchMap(() => dispatchMessage({type: 'disconnect'})))
121
- .subscribe()
122
- }
123
-
124
- return [incomingEvents$.pipe(share()), dispatchMessage]
191
+ return [incomingEvents$, dispatchMessage, connections$, createUnload()]
125
192
  }
@@ -0,0 +1,70 @@
1
+ import {type Path} from '@sanity/types'
2
+ import {describe, expect, it} from 'vitest'
3
+
4
+ import {startsWithPath} from './paths'
5
+
6
+ describe('startsWithPath', () => {
7
+ it('treats the empty path as a prefix of everything, which is the document root', () => {
8
+ expect(startsWithPath([], ['title'])).toBe(true)
9
+ expect(startsWithPath([], [])).toBe(true)
10
+ })
11
+
12
+ it('matches a path against itself', () => {
13
+ expect(startsWithPath(['title'], ['title'])).toBe(true)
14
+ })
15
+
16
+ it('matches a descendant', () => {
17
+ expect(startsWithPath(['body'], ['body', 'children'])).toBe(true)
18
+ })
19
+
20
+ it('does not match an ancestor, since a longer prefix cannot fit', () => {
21
+ expect(startsWithPath(['body', 'children'], ['body'])).toBe(false)
22
+ })
23
+
24
+ it('does not match a sibling', () => {
25
+ expect(startsWithPath(['title'], ['subtitle'])).toBe(false)
26
+ })
27
+
28
+ it('does not match a partial segment name', () => {
29
+ // `title` must not be treated as a prefix of `titleImage`.
30
+ expect(startsWithPath(['title'], ['titleImage'])).toBe(false)
31
+ })
32
+
33
+ it('compares keyed segments by key rather than by identity', () => {
34
+ // Distinct objects with the same key, which is what arrives over the wire.
35
+ expect(startsWithPath(['cast', {_key: 'a'}], ['cast', {_key: 'a'}, 'name'])).toBe(true)
36
+ expect(startsWithPath(['cast', {_key: 'a'}], ['cast', {_key: 'b'}, 'name'])).toBe(false)
37
+ })
38
+
39
+ it('does not confuse a keyed segment with a string or a number', () => {
40
+ expect(startsWithPath([{_key: 'a'}], ['a'])).toBe(false)
41
+ expect(startsWithPath(['a'], [{_key: 'a'}])).toBe(false)
42
+ expect(startsWithPath([{_key: '0'}], [0])).toBe(false)
43
+ })
44
+
45
+ it('compares numeric indexes', () => {
46
+ expect(startsWithPath(['awards', 0], ['awards', 0])).toBe(true)
47
+ expect(startsWithPath(['awards', 0], ['awards', 1])).toBe(false)
48
+ })
49
+
50
+ it('compares index tuples structurally', () => {
51
+ const prefix: Path = ['body', [0, 2]]
52
+ expect(startsWithPath(prefix, ['body', [0, 2], 'children'])).toBe(true)
53
+ expect(startsWithPath(prefix, ['body', [0, 3]])).toBe(false)
54
+ expect(startsWithPath(prefix, ['body', [0, '']])).toBe(false)
55
+ })
56
+
57
+ it('does not confuse a tuple with a keyed segment', () => {
58
+ expect(startsWithPath([[0, 2]], [{_key: 'a'}])).toBe(false)
59
+ expect(startsWithPath([{_key: 'a'}], [[0, 2]])).toBe(false)
60
+ })
61
+
62
+ it('handles a realistic Portable Text span path', () => {
63
+ const field: Path = ['body']
64
+ const span: Path = ['body', {_key: 'block-1'}, 'children', {_key: 'span-1'}, 'text']
65
+
66
+ expect(startsWithPath(field, span)).toBe(true)
67
+ expect(startsWithPath(['title'], span)).toBe(false)
68
+ expect(startsWithPath(['body', {_key: 'block-2'}], span)).toBe(false)
69
+ })
70
+ })
@@ -0,0 +1,41 @@
1
+ import {type IndexTuple, type KeyedSegment, type Path, type PathSegment} from '@sanity/types'
2
+
3
+ /**
4
+ * Segments are not always strings, despite what `PresenceLocation.path` declares.
5
+ * Array items arrive as `{_key}` and Portable Text spans as a mix of keys and
6
+ * property names, because that is what the Studio sends.
7
+ */
8
+ function isKeyedSegment(segment: PathSegment): segment is KeyedSegment {
9
+ return (
10
+ typeof segment === 'object' && segment !== null && !Array.isArray(segment) && '_key' in segment
11
+ )
12
+ }
13
+
14
+ /**
15
+ * Index tuples (`[from, to]`) only occur in slice selections, which presence never
16
+ * reports. Compared structurally rather than assumed absent.
17
+ */
18
+ function isEqualTuple(a: IndexTuple, b: IndexTuple): boolean {
19
+ return a.length === b.length && a.every((value, index) => value === b[index])
20
+ }
21
+
22
+ /** Compares two path segments, whatever shape they arrive in. */
23
+ function isEqualSegment(a: PathSegment, b: PathSegment): boolean {
24
+ if (a === b) return true
25
+ if (isKeyedSegment(a) && isKeyedSegment(b)) return a._key === b._key
26
+ if (Array.isArray(a) && Array.isArray(b)) return isEqualTuple(a, b)
27
+ return false
28
+ }
29
+
30
+ /**
31
+ * True when `candidate` is at or below `prefix` in the document tree.
32
+ *
33
+ * Equivalent to `startsWith` from the Studio's `@sanity/util/paths`, which is not
34
+ * an SDK dependency.
35
+ *
36
+ * @internal
37
+ */
38
+ export function startsWithPath(prefix: Path, candidate: Path): boolean {
39
+ if (prefix.length > candidate.length) return false
40
+ return prefix.every((segment, index) => isEqualSegment(segment, candidate[index]))
41
+ }