@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.
- package/README.md +12 -0
- package/dist/_exports/_internal.d.ts +71 -3
- package/dist/_exports/_internal.d.ts.map +1 -1
- package/dist/_exports/_internal.js +332 -24
- package/dist/_exports/_internal.js.map +1 -1
- package/dist/_exports/agent.d.ts +65 -0
- package/dist/_exports/agent.d.ts.map +1 -0
- package/dist/_exports/agent.js +71 -0
- package/dist/_exports/agent.js.map +1 -0
- package/dist/_exports/comlink.d.ts +118 -0
- package/dist/_exports/comlink.d.ts.map +1 -0
- package/dist/_exports/comlink.js +69 -0
- package/dist/_exports/comlink.js.map +1 -0
- package/dist/authStore-C0o1NOxW.d.ts +3165 -0
- package/dist/authStore-C0o1NOxW.d.ts.map +1 -0
- package/dist/authStore-CtE9bLHW.js +754 -0
- package/dist/authStore-CtE9bLHW.js.map +1 -0
- package/dist/clientStore-BtE2-q2i.js +90 -0
- package/dist/clientStore-BtE2-q2i.js.map +1 -0
- package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
- package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
- package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
- package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
- package/dist/getEnv-D5RGkQLu.js +8 -0
- package/dist/getEnv-D5RGkQLu.js.map +1 -0
- package/dist/getNodeState-CR3__B2D.js +67 -0
- package/dist/getNodeState-CR3__B2D.js.map +1 -0
- package/dist/index.d.ts +3 -2335
- package/dist/index.js +4048 -3878
- package/dist/index.js.map +1 -1
- package/dist/logger-DAtJb5RQ.js +173 -0
- package/dist/logger-DAtJb5RQ.js.map +1 -0
- package/dist/object-CxXlSVg7.js +83 -0
- package/dist/object-CxXlSVg7.js.map +1 -0
- package/dist/queryStore-C7yMHPUI.js +1346 -0
- package/dist/queryStore-C7yMHPUI.js.map +1 -0
- package/dist/rolldown-runtime-vyAXikos.js +9 -0
- package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
- package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
- package/dist/telemetryManager-UIEiMtC0.js +127 -0
- package/dist/telemetryManager-UIEiMtC0.js.map +1 -0
- package/dist/version-D8RP_Q59.js +9 -0
- package/dist/version-D8RP_Q59.js.map +1 -0
- package/package.json +11 -17
- package/src/_exports/_internal.ts +1 -0
- package/src/_exports/agent.ts +19 -0
- package/src/_exports/comlink.ts +16 -0
- package/src/_exports/index.ts +45 -90
- package/src/applications/applications.test-d.ts +5 -0
- package/src/applications/applications.ts +5 -2
- package/src/auth/authMode.test.ts +0 -25
- package/src/auth/authMode.ts +3 -6
- package/src/auth/authStore.test.ts +1 -1
- package/src/auth/authStore.ts +0 -1
- package/src/comments/addonDatasetStore.test.ts +215 -0
- package/src/comments/addonDatasetStore.ts +249 -0
- package/src/comments/buildCommentThreads.test.ts +83 -0
- package/src/comments/buildCommentThreads.ts +54 -0
- package/src/comments/commentActions.test.ts +540 -0
- package/src/comments/commentActions.ts +555 -0
- package/src/comments/commentFieldPath.test.ts +40 -0
- package/src/comments/commentFieldPath.ts +44 -0
- package/src/comments/commentsConstants.ts +80 -0
- package/src/comments/commentsStore.test.ts +419 -0
- package/src/comments/commentsStore.ts +483 -0
- package/src/comments/normalizeComment.test.ts +116 -0
- package/src/comments/normalizeComment.ts +45 -0
- package/src/comments/observeComments.test.ts +193 -0
- package/src/comments/observeComments.ts +113 -0
- package/src/comments/reducers.test.ts +277 -0
- package/src/comments/reducers.ts +316 -0
- package/src/comments/types.ts +249 -0
- package/src/comments/weakenReferences.test.ts +47 -0
- package/src/comments/weakenReferences.ts +29 -0
- package/src/config/sanityConfig.ts +3 -75
- package/src/datasets/datasets.test.ts +44 -9
- package/src/datasets/datasets.ts +24 -23
- package/src/document/applyDocumentActions.test.ts +8 -12
- package/src/document/documentConstants.ts +17 -0
- package/src/document/documentStore.test.ts +173 -19
- package/src/document/documentStore.ts +36 -5
- package/src/document/patchOperations.test.ts +27 -0
- package/src/document/patchOperations.ts +10 -1
- package/src/document/permissions.test.ts +3 -1
- package/src/document/reducers.test.ts +357 -0
- package/src/document/reducers.ts +125 -27
- package/src/document/util.test.ts +46 -0
- package/src/document/util.ts +37 -1
- package/src/favorites/favorites.test.ts +64 -24
- package/src/favorites/favorites.ts +73 -22
- package/src/installations/installations.test-d.ts +5 -0
- package/src/installations/installations.ts +4 -0
- package/src/presence/bifurTransport.test.ts +155 -3
- package/src/presence/bifurTransport.ts +79 -12
- package/src/presence/paths.test.ts +70 -0
- package/src/presence/paths.ts +41 -0
- package/src/presence/presenceStore.test.ts +822 -2
- package/src/presence/presenceStore.ts +518 -102
- package/src/presence/types.ts +175 -3
- package/src/preview/types.ts +0 -25
- package/src/projection/projectionStore.test.ts +0 -8
- package/src/projection/types.ts +0 -8
- package/src/query/queryStore.ts +7 -6
- package/src/store/createActionBinder.test.ts +51 -29
- package/src/store/createActionBinder.ts +16 -62
- package/src/store/createSanityInstance.test.ts +0 -68
- package/src/store/createSanityInstance.ts +0 -63
- package/src/store/createStateSourceAction.ts +1 -1
- package/src/store/fetcherStore.test.ts +23 -1
- package/src/store/fetcherStore.ts +3 -3
- package/src/users/reducers.test.ts +0 -3
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts +0 -1147
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +0 -1
- package/dist/_chunks-es/_internal.js +0 -134
- package/dist/_chunks-es/_internal.js.map +0 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +0 -1864
- package/dist/_chunks-es/createGroqSearchFilter.js.map +0 -1
- package/dist/_chunks-es/telemetryManager.js +0 -93
- package/dist/_chunks-es/telemetryManager.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/src/applications/userApplications.test.ts +0 -126
- package/src/applications/userApplications.ts +0 -261
- package/src/preview/getPreviewState.test.ts +0 -137
- package/src/preview/getPreviewState.ts +0 -69
- package/src/preview/resolvePreview.test.ts +0 -64
- package/src/preview/resolvePreview.ts +0 -43
- package/src/utils/createFetcherStore.test.ts +0 -171
- 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
|
|
99
|
-
|
|
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
|
-
|
|
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
|
+
}
|