@sanity/sdk 2.8.0 → 2.10.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/dist/_chunks-dts/utils.d.ts +2450 -0
- package/dist/_chunks-es/_internal.js +129 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1537 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +87 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_chunks-es/version.js +7 -0
- package/dist/_chunks-es/version.js.map +1 -0
- package/dist/_exports/_internal.d.ts +64 -0
- package/dist/_exports/_internal.js +20 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +2 -2343
- package/dist/index.js +465 -1813
- package/dist/index.js.map +1 -1
- package/package.json +17 -12
- package/src/_exports/_internal.ts +14 -0
- package/src/_exports/index.ts +18 -1
- package/src/auth/authStore.test.ts +150 -1
- package/src/auth/authStore.ts +11 -11
- package/src/auth/dashboardAuth.ts +2 -2
- package/src/auth/handleAuthCallback.ts +9 -3
- package/src/auth/logout.test.ts +1 -1
- package/src/auth/logout.ts +1 -1
- package/src/auth/refreshStampedToken.test.ts +118 -1
- package/src/auth/refreshStampedToken.ts +3 -2
- package/src/auth/standaloneAuth.ts +9 -3
- package/src/auth/studioAuth.ts +34 -7
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +10 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +5 -1
- package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
- package/src/auth/utils.ts +33 -0
- package/src/client/clientStore.test.ts +44 -30
- package/src/client/clientStore.ts +49 -48
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/sanityConfig.ts +78 -12
- package/src/document/actions.ts +18 -11
- package/src/document/applyDocumentActions.test.ts +7 -6
- package/src/document/applyDocumentActions.ts +10 -4
- package/src/document/documentStore.test.ts +542 -188
- package/src/document/documentStore.ts +142 -76
- package/src/document/events.ts +7 -2
- package/src/document/permissions.test.ts +18 -16
- package/src/document/permissions.ts +35 -11
- package/src/document/processActions.test.ts +359 -32
- package/src/document/processActions.ts +106 -78
- package/src/document/reducers.test.ts +117 -29
- package/src/document/reducers.ts +47 -40
- package/src/document/sharedListener.ts +16 -6
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +9 -2
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +19 -2
- package/src/presence/presenceStore.test.ts +96 -0
- package/src/presence/presenceStore.ts +96 -24
- package/src/preview/getPreviewState.test.ts +115 -98
- package/src/preview/getPreviewState.ts +38 -60
- package/src/preview/previewProjectionUtils.test.ts +179 -0
- package/src/preview/previewProjectionUtils.ts +93 -0
- package/src/preview/resolvePreview.test.ts +42 -25
- package/src/preview/resolvePreview.ts +33 -10
- package/src/preview/{previewStore.ts → types.ts} +8 -17
- package/src/projection/getProjectionState.test.ts +16 -16
- package/src/projection/getProjectionState.ts +6 -5
- package/src/projection/projectionQuery.ts +2 -3
- package/src/projection/projectionStore.test.ts +2 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +12 -11
- package/src/projection/types.ts +1 -1
- package/src/query/queryStore.test.ts +12 -12
- package/src/query/queryStore.ts +12 -11
- package/src/query/reducers.ts +3 -3
- package/src/releases/getPerspectiveState.ts +7 -6
- package/src/releases/releasesStore.test.ts +20 -5
- package/src/releases/releasesStore.ts +20 -8
- package/src/store/createActionBinder.test.ts +31 -31
- package/src/store/createActionBinder.ts +43 -38
- package/src/store/createSanityInstance.ts +2 -3
- package/src/store/createStateSourceAction.test.ts +62 -0
- package/src/store/createStateSourceAction.ts +34 -39
- package/src/telemetry/__telemetry__/sdk.telemetry.ts +42 -0
- package/src/telemetry/devMode.test.ts +52 -0
- package/src/telemetry/devMode.ts +40 -0
- package/src/telemetry/initTelemetry.test.ts +225 -0
- package/src/telemetry/initTelemetry.ts +205 -0
- package/src/telemetry/telemetryManager.test.ts +263 -0
- package/src/telemetry/telemetryManager.ts +187 -0
- package/src/users/reducers.ts +3 -4
- package/src/users/usersStore.test.ts +1 -0
- package/src/users/usersStore.ts +5 -1
- package/src/utils/createFetcherStore.test.ts +6 -4
- package/src/utils/createFetcherStore.ts +8 -5
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
- package/src/utils/setCleanupTimeout.ts +24 -0
- package/src/preview/previewQuery.test.ts +0 -236
- package/src/preview/previewQuery.ts +0 -153
- package/src/preview/previewStore.test.ts +0 -36
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
- package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
- package/src/preview/util.ts +0 -13
package/src/document/reducers.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {getPublishedId} from '@sanity/
|
|
1
|
+
import {DocumentId, getDraftId, getPublishedId, getVersionId} from '@sanity/id-utils'
|
|
2
2
|
import {type Mutation, type PatchOperations, type SanityDocumentLike} from '@sanity/types'
|
|
3
|
-
import {omit} from 'lodash-es'
|
|
4
3
|
|
|
4
|
+
import {type DocumentHandle} from '../config/sanityConfig'
|
|
5
|
+
import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
|
|
5
6
|
import {type StoreContext} from '../store/defineStore'
|
|
6
|
-
import {
|
|
7
|
+
import {insecureRandomId} from '../utils/ids'
|
|
8
|
+
import {omitProperty} from '../utils/object'
|
|
9
|
+
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
7
10
|
import {type DocumentAction} from './actions'
|
|
8
11
|
import {DOCUMENT_STATE_CLEAR_DELAY} from './documentConstants'
|
|
9
12
|
import {type DocumentState, type DocumentStoreState} from './documentStore'
|
|
@@ -18,9 +21,13 @@ export type SyncTransactionState = Pick<
|
|
|
18
21
|
'queued' | 'applied' | 'documentStates' | 'outgoing' | 'grants'
|
|
19
22
|
>
|
|
20
23
|
|
|
24
|
+
type DocumentHandleLike = Pick<DocumentHandle, 'perspective'> & {
|
|
25
|
+
documentId?: string
|
|
26
|
+
liveEdit?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
type ActionMap = {
|
|
22
30
|
create: 'sanity.action.document.version.create'
|
|
23
|
-
createLiveEdit: 'sanity.action.document.create'
|
|
24
31
|
discard: 'sanity.action.document.version.discard'
|
|
25
32
|
unpublish: 'sanity.action.document.unpublish'
|
|
26
33
|
delete: 'sanity.action.document.delete'
|
|
@@ -35,7 +42,6 @@ type OptimisticLock = {
|
|
|
35
42
|
|
|
36
43
|
export type HttpAction =
|
|
37
44
|
| {actionType: ActionMap['create']; publishedId: string; attributes: SanityDocumentLike}
|
|
38
|
-
| {actionType: ActionMap['createLiveEdit']; publishedId: string; attributes: SanityDocumentLike}
|
|
39
45
|
| {actionType: ActionMap['discard']; versionId: string; purge?: boolean}
|
|
40
46
|
| {actionType: ActionMap['unpublish']; draftId: string; publishedId: string}
|
|
41
47
|
| {actionType: ActionMap['delete']; publishedId: string; includeDrafts?: string[]}
|
|
@@ -115,9 +121,9 @@ export interface AppliedTransaction extends QueuedTransaction {
|
|
|
115
121
|
outgoingActions: HttpAction[]
|
|
116
122
|
|
|
117
123
|
/**
|
|
118
|
-
* similar to `outgoingActions` but comprised of mutations instead of
|
|
119
|
-
*
|
|
120
|
-
*
|
|
124
|
+
* similar to `outgoingActions` but comprised of mutations instead of actions.
|
|
125
|
+
* Useful for debugging, and is also used by liveEdit documents to send mutations,
|
|
126
|
+
* since they can't use the Actions API which is pretty dependent on the draft model.
|
|
121
127
|
*/
|
|
122
128
|
outgoingMutations: Mutation[]
|
|
123
129
|
}
|
|
@@ -145,7 +151,7 @@ export function queueTransaction(
|
|
|
145
151
|
transaction: QueuedTransaction,
|
|
146
152
|
): SyncTransactionState {
|
|
147
153
|
const {transactionId, actions} = transaction
|
|
148
|
-
const prevWithSubscriptionIds =
|
|
154
|
+
const prevWithSubscriptionIds = getDocumentIdsFromHandleLikes(actions).reduce(
|
|
149
155
|
(acc, id) => addSubscriptionIdToDocument(acc, id, transactionId),
|
|
150
156
|
prev,
|
|
151
157
|
)
|
|
@@ -163,7 +169,7 @@ export function removeQueuedTransaction(
|
|
|
163
169
|
const transaction = prev.queued.find((t) => t.transactionId === transactionId)
|
|
164
170
|
if (!transaction) return prev
|
|
165
171
|
|
|
166
|
-
const prevWithSubscriptionIds =
|
|
172
|
+
const prevWithSubscriptionIds = getDocumentIdsFromHandleLikes(transaction.actions).reduce(
|
|
167
173
|
(acc, id) => removeSubscriptionIdFromDocument(acc, id, transactionId),
|
|
168
174
|
prev,
|
|
169
175
|
)
|
|
@@ -179,7 +185,7 @@ export function applyFirstQueuedTransaction(prev: SyncTransactionState): SyncTra
|
|
|
179
185
|
if (!queued) return prev
|
|
180
186
|
if (!prev.grants) return prev
|
|
181
187
|
|
|
182
|
-
const ids =
|
|
188
|
+
const ids = getDocumentIdsFromHandleLikes(queued.actions)
|
|
183
189
|
// the local value is only ever `undefined` if it has not been loaded yet
|
|
184
190
|
// we can't get the next applied state unless all relevant documents are ready
|
|
185
191
|
if (ids.some((id) => prev.documentStates[id]?.local === undefined)) return prev
|
|
@@ -265,6 +271,9 @@ export function batchAppliedTransactions([curr, ...rest]: AppliedTransaction[]):
|
|
|
265
271
|
if (!next) return undefined
|
|
266
272
|
if (next.disableBatching) return editAction
|
|
267
273
|
|
|
274
|
+
// Don't batch a liveEdit edit with a non-liveEdit edit — they route to different APIs
|
|
275
|
+
if (!!action.liveEdit !== !!next.actions[0]?.liveEdit) return editAction
|
|
276
|
+
|
|
268
277
|
return {
|
|
269
278
|
disableBatching: false,
|
|
270
279
|
// Use the transactionId from the later (next) transaction.
|
|
@@ -337,7 +346,7 @@ export function cleanupOutgoingTransaction(prev: SyncTransactionState): SyncTran
|
|
|
337
346
|
if (!outgoing) return prev
|
|
338
347
|
|
|
339
348
|
let next = prev
|
|
340
|
-
const ids =
|
|
349
|
+
const ids = getDocumentIdsFromHandleLikes(outgoing.actions)
|
|
341
350
|
for (const transactionId of outgoing.batchedTransactionIds) {
|
|
342
351
|
for (const documentId of ids) {
|
|
343
352
|
next = removeSubscriptionIdFromDocument(next, documentId, transactionId)
|
|
@@ -383,7 +392,7 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
|
|
|
383
392
|
local: documentId in working ? working[documentId] : local,
|
|
384
393
|
unverifiedRevisions:
|
|
385
394
|
prev.outgoing && prev.outgoing.transactionId in unverifiedRevisions
|
|
386
|
-
?
|
|
395
|
+
? omitProperty(unverifiedRevisions, prev.outgoing.transactionId)
|
|
387
396
|
: unverifiedRevisions,
|
|
388
397
|
}
|
|
389
398
|
return [documentId, next] as const
|
|
@@ -411,7 +420,7 @@ export function applyRemoteDocument(
|
|
|
411
420
|
const revisionToVerify = revision ? prevUnverifiedRevisions?.[revision] : undefined
|
|
412
421
|
let unverifiedRevisions = prevUnverifiedRevisions ?? EMPTY_REVISIONS
|
|
413
422
|
if (revision && revisionToVerify) {
|
|
414
|
-
unverifiedRevisions =
|
|
423
|
+
unverifiedRevisions = omitProperty(prevUnverifiedRevisions, revision)
|
|
415
424
|
}
|
|
416
425
|
|
|
417
426
|
// if this remote document is from a `'sync'` event (meaning that the whole
|
|
@@ -538,7 +547,7 @@ export function removeSubscriptionIdFromDocument(
|
|
|
538
547
|
|
|
539
548
|
if (!prevDocState) return prev
|
|
540
549
|
if (!subscriptions.length) {
|
|
541
|
-
return {...prev, documentStates:
|
|
550
|
+
return {...prev, documentStates: omitProperty(prev.documentStates, documentId)}
|
|
542
551
|
}
|
|
543
552
|
return {
|
|
544
553
|
...prev,
|
|
@@ -551,22 +560,10 @@ export function removeSubscriptionIdFromDocument(
|
|
|
551
560
|
|
|
552
561
|
export function manageSubscriberIds(
|
|
553
562
|
{state}: StoreContext<SyncTransactionState>,
|
|
554
|
-
|
|
555
|
-
options?: {expandDraftPublished?: boolean},
|
|
563
|
+
handles: DocumentHandleLike[],
|
|
556
564
|
): () => void {
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
new Set(
|
|
560
|
-
expandDraftPublished
|
|
561
|
-
? (Array.isArray(documentId) ? documentId : [documentId]).flatMap((id) => [
|
|
562
|
-
getPublishedId(id),
|
|
563
|
-
getDraftId(id),
|
|
564
|
-
])
|
|
565
|
-
: Array.isArray(documentId)
|
|
566
|
-
? documentId
|
|
567
|
-
: [documentId],
|
|
568
|
-
),
|
|
569
|
-
)
|
|
565
|
+
const documentIds = getDocumentIdsFromHandleLikes(handles)
|
|
566
|
+
|
|
570
567
|
const subscriptionId = insecureRandomId()
|
|
571
568
|
state.set('addSubscribers', (prev) =>
|
|
572
569
|
documentIds.reduce(
|
|
@@ -576,7 +573,7 @@ export function manageSubscriberIds(
|
|
|
576
573
|
)
|
|
577
574
|
|
|
578
575
|
return () => {
|
|
579
|
-
|
|
576
|
+
setCleanupTimeout(() => {
|
|
580
577
|
state.set('removeSubscribers', (prev) =>
|
|
581
578
|
documentIds.reduce(
|
|
582
579
|
(acc, id) => removeSubscriptionIdFromDocument(acc, id, subscriptionId),
|
|
@@ -587,13 +584,23 @@ export function manageSubscriberIds(
|
|
|
587
584
|
}
|
|
588
585
|
}
|
|
589
586
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
)
|
|
598
|
-
|
|
587
|
+
// document handles are passed in via the public facing API, but we also need to
|
|
588
|
+
// pull the correct document ids from action bodies, which have similar but not
|
|
589
|
+
// identical shapes to the document handles.
|
|
590
|
+
function getDocumentIdsFromHandleLikes(handles: DocumentHandleLike[]): string[] {
|
|
591
|
+
return handles.flatMap((handle) => {
|
|
592
|
+
const idsForDocument = []
|
|
593
|
+
if (!handle.documentId) return []
|
|
594
|
+
if (handle.liveEdit) {
|
|
595
|
+
return [handle.documentId]
|
|
596
|
+
}
|
|
597
|
+
if (isReleasePerspective(handle.perspective)) {
|
|
598
|
+
idsForDocument.push(
|
|
599
|
+
getVersionId(DocumentId(handle.documentId), handle.perspective.releaseName),
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
idsForDocument.push(getPublishedId(DocumentId(handle.documentId)))
|
|
603
|
+
idsForDocument.push(getDraftId(DocumentId(handle.documentId)))
|
|
604
|
+
return idsForDocument
|
|
605
|
+
})
|
|
599
606
|
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from 'rxjs'
|
|
15
15
|
|
|
16
16
|
import {getClientState} from '../client/clientStore'
|
|
17
|
+
import {type DocumentResource, isDatasetResource} from '../config/sanityConfig'
|
|
17
18
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
18
19
|
|
|
19
20
|
const API_VERSION = 'v2025-05-06'
|
|
@@ -23,10 +24,15 @@ export interface SharedListener {
|
|
|
23
24
|
dispose: () => void
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export function createSharedListener(
|
|
27
|
+
export function createSharedListener(
|
|
28
|
+
instance: SanityInstance,
|
|
29
|
+
resource?: DocumentResource,
|
|
30
|
+
): SharedListener {
|
|
27
31
|
const dispose$ = new Subject<void>()
|
|
28
32
|
const events$ = getClientState(instance, {
|
|
29
33
|
apiVersion: API_VERSION,
|
|
34
|
+
// TODO: remove in v3 when we're ready for everything to be queried via resource
|
|
35
|
+
resource: resource && !isDatasetResource(resource) ? resource : undefined,
|
|
30
36
|
}).observable.pipe(
|
|
31
37
|
switchMap((client) =>
|
|
32
38
|
// TODO: it seems like the client.listen method is not emitting disconnected
|
|
@@ -62,13 +68,17 @@ export function createSharedListener(instance: SanityInstance): SharedListener {
|
|
|
62
68
|
}
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
export function createFetchDocument(instance: SanityInstance) {
|
|
71
|
+
export function createFetchDocument(instance: SanityInstance, resource?: DocumentResource) {
|
|
66
72
|
return function (documentId: string): Observable<SanityDocument | null> {
|
|
67
|
-
return getClientState(instance, {
|
|
73
|
+
return getClientState(instance, {
|
|
74
|
+
apiVersion: API_VERSION,
|
|
75
|
+
// TODO: remove in v3 when we're ready for everything to be queried via resource
|
|
76
|
+
resource: resource && !isDatasetResource(resource) ? resource : undefined,
|
|
77
|
+
}).observable.pipe(
|
|
68
78
|
switchMap((client) => {
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
const loadDocument = createDocumentLoaderFromClient(client
|
|
79
|
+
// creates a observable request to the /doc/{documentId} endpoint for a given document id
|
|
80
|
+
// should work across all kinds of document IDs (drafts.**, version.**., etc.)
|
|
81
|
+
const loadDocument = createDocumentLoaderFromClient(client)
|
|
72
82
|
return loadDocument(documentId)
|
|
73
83
|
}),
|
|
74
84
|
map((result) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {DocumentId, getPublishedId, getVersionId} from '@sanity/id-utils'
|
|
2
|
+
|
|
3
|
+
import {type DocumentHandle} from '../config/sanityConfig'
|
|
4
|
+
import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
|
|
5
|
+
|
|
6
|
+
export function getEffectiveDocumentId(doc: DocumentHandle): string {
|
|
7
|
+
if (doc.liveEdit) {
|
|
8
|
+
return doc.documentId
|
|
9
|
+
} else if (isReleasePerspective(doc.perspective)) {
|
|
10
|
+
return getVersionId(DocumentId(doc.documentId), doc.perspective.releaseName)
|
|
11
|
+
} else {
|
|
12
|
+
return getPublishedId(DocumentId(doc.documentId))
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -126,9 +126,15 @@ describe('favoritesStore', () => {
|
|
|
126
126
|
})
|
|
127
127
|
|
|
128
128
|
it('handles error and returns default response', async () => {
|
|
129
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
129
130
|
setupMockStateSource({fetchImpl: vi.fn().mockRejectedValue(new Error('Failed to fetch'))})
|
|
130
131
|
const result = await resolveFavoritesState(instance!, mockContext)
|
|
131
132
|
expect(result).toEqual({isFavorited: false})
|
|
133
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
134
|
+
'Favorites service connection error',
|
|
135
|
+
expect.any(Error),
|
|
136
|
+
)
|
|
137
|
+
consoleErrorSpy.mockRestore()
|
|
132
138
|
})
|
|
133
139
|
|
|
134
140
|
it('shares observable between multiple subscribers and cleans up', async () => {
|
|
@@ -140,8 +146,9 @@ describe('favoritesStore', () => {
|
|
|
140
146
|
await firstValueFrom(state.observable)
|
|
141
147
|
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
142
148
|
// Second subscriber should use cached response
|
|
143
|
-
const
|
|
144
|
-
|
|
149
|
+
const state2 = getFavoritesState(instance!, mockContext)
|
|
150
|
+
const sub2 = state2.subscribe()
|
|
151
|
+
await firstValueFrom(state2.observable)
|
|
145
152
|
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
146
153
|
// Cleanup
|
|
147
154
|
sub1()
|
|
@@ -45,16 +45,18 @@ describe('createBifurTransport', () => {
|
|
|
45
45
|
|
|
46
46
|
beforeEach(() => {
|
|
47
47
|
vi.useFakeTimers()
|
|
48
|
+
vi.clearAllMocks()
|
|
48
49
|
mockBifurClient = {
|
|
49
50
|
listen: vi.fn(() => new Subject<never>()),
|
|
50
51
|
request: vi.fn(() => of(undefined)),
|
|
51
52
|
}
|
|
52
53
|
fromUrlMock.mockReturnValue(mockBifurClient)
|
|
53
54
|
|
|
55
|
+
// Default mock is a dataset client using project hostname
|
|
54
56
|
mockSanityClient = {
|
|
55
57
|
config: () => ({
|
|
56
58
|
dataset: 'test-dataset',
|
|
57
|
-
url: '
|
|
59
|
+
url: 'https://test-project.api.sanity.io/v2022-06-30',
|
|
58
60
|
requestTagPrefix: 'test-tag',
|
|
59
61
|
}),
|
|
60
62
|
withConfig: vi.fn().mockReturnThis(),
|
|
@@ -63,7 +65,7 @@ describe('createBifurTransport', () => {
|
|
|
63
65
|
token$ = new Subject<string | null>()
|
|
64
66
|
})
|
|
65
67
|
|
|
66
|
-
it('constructs the bifur client
|
|
68
|
+
it('constructs the bifur client URL for a dataset resource', () => {
|
|
67
69
|
createBifurTransport({
|
|
68
70
|
client: mockSanityClient,
|
|
69
71
|
token$,
|
|
@@ -71,13 +73,51 @@ describe('createBifurTransport', () => {
|
|
|
71
73
|
})
|
|
72
74
|
|
|
73
75
|
expect(fromUrlMock).toHaveBeenCalledWith(
|
|
74
|
-
'
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
'wss://test-project.api.sanity.io/v2022-06-30/socket/test-dataset?tag=test-tag',
|
|
77
|
+
{token$},
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('constructs the bifur client URL for a canvas resource', () => {
|
|
82
|
+
const canvasClient = {
|
|
83
|
+
config: () => ({
|
|
84
|
+
resource: {type: 'canvas', id: 'canvas-123'},
|
|
85
|
+
url: 'https://api.sanity.io/v2022-06-30',
|
|
86
|
+
requestTagPrefix: 'test-tag',
|
|
87
|
+
}),
|
|
88
|
+
withConfig: vi.fn().mockReturnThis(),
|
|
89
|
+
} as unknown as SanityClient
|
|
90
|
+
|
|
91
|
+
createBifurTransport({
|
|
92
|
+
client: canvasClient,
|
|
93
|
+
token$,
|
|
94
|
+
sessionId: 'session-id-123',
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
expect(fromUrlMock).toHaveBeenCalledWith(
|
|
98
|
+
'wss://api.sanity.io/v2022-06-30/socket/canvases/canvas-123?tag=test-tag',
|
|
99
|
+
{token$},
|
|
78
100
|
)
|
|
79
101
|
})
|
|
80
102
|
|
|
103
|
+
it('throws when no canvas resource or dataset is configured', () => {
|
|
104
|
+
const invalidClient = {
|
|
105
|
+
config: () => ({
|
|
106
|
+
url: 'https://api.sanity.io/v2022-06-30',
|
|
107
|
+
requestTagPrefix: 'test-tag',
|
|
108
|
+
}),
|
|
109
|
+
withConfig: vi.fn().mockReturnThis(),
|
|
110
|
+
} as unknown as SanityClient
|
|
111
|
+
|
|
112
|
+
expect(() =>
|
|
113
|
+
createBifurTransport({
|
|
114
|
+
client: invalidClient,
|
|
115
|
+
token$,
|
|
116
|
+
sessionId: 'session-id-123',
|
|
117
|
+
}),
|
|
118
|
+
).toThrow('Unable to determine presence URL')
|
|
119
|
+
})
|
|
120
|
+
|
|
81
121
|
it('handles incoming rollCall events', () => {
|
|
82
122
|
const incomingBifurEvents$ = new Subject<IncomingBifurEvent>()
|
|
83
123
|
mockBifurClient.listen.mockReturnValue(incomingBifurEvents$)
|
|
@@ -36,8 +36,25 @@ type IncomingBifurEvent = RollCallEvent | BifurStateMessage | BifurDisconnectMes
|
|
|
36
36
|
|
|
37
37
|
function getBifurClient(client: SanityClient, token$: Observable<string | null>): BifurClient {
|
|
38
38
|
const bifurVersionedClient = client.withConfig({apiVersion: '2022-06-30'})
|
|
39
|
-
const {
|
|
40
|
-
|
|
39
|
+
const {
|
|
40
|
+
resource,
|
|
41
|
+
dataset,
|
|
42
|
+
url: baseUrl,
|
|
43
|
+
requestTagPrefix = 'sanity.sdk.presence',
|
|
44
|
+
} = bifurVersionedClient.config()
|
|
45
|
+
|
|
46
|
+
let resourcePath: string
|
|
47
|
+
if (resource?.type === 'canvas') {
|
|
48
|
+
resourcePath = `canvases/${resource.id}`
|
|
49
|
+
} else if (dataset) {
|
|
50
|
+
// Dataset clients use project hostname — dataset name alone is the socket path
|
|
51
|
+
resourcePath = dataset
|
|
52
|
+
} else {
|
|
53
|
+
throw new Error(`Unable to determine presence URL: no canvas resource or dataset configured`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const url = `${baseUrl}/socket/${resourcePath}`.replace(/^http/, 'ws')
|
|
57
|
+
|
|
41
58
|
const urlWithTag = `${url}?tag=${requestTagPrefix}`
|
|
42
59
|
|
|
43
60
|
return fromUrl(urlWithTag, {token$})
|
|
@@ -48,6 +48,9 @@ describe('presenceStore', () => {
|
|
|
48
48
|
|
|
49
49
|
mockClient = {
|
|
50
50
|
withConfig: vi.fn().mockReturnThis(),
|
|
51
|
+
observable: {
|
|
52
|
+
request: vi.fn(() => of({organizationId: 'test-org-id'})),
|
|
53
|
+
},
|
|
51
54
|
} as unknown as SanityClient
|
|
52
55
|
|
|
53
56
|
mockTokenState = new Subject<string | null>()
|
|
@@ -243,5 +246,98 @@ describe('presenceStore', () => {
|
|
|
243
246
|
|
|
244
247
|
unsubscribe()
|
|
245
248
|
})
|
|
249
|
+
|
|
250
|
+
it('should throw an error when initialized with a media library resource', () => {
|
|
251
|
+
const mediaLibraryResource = {mediaLibraryId: 'ml123'}
|
|
252
|
+
|
|
253
|
+
expect(() => {
|
|
254
|
+
getPresence(instance, {resource: mediaLibraryResource})
|
|
255
|
+
}).toThrow('Presence is not supported for media library resources.')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should work with a dataset resource', () => {
|
|
259
|
+
const datasetResource = {projectId: 'test-project', dataset: 'test-dataset'}
|
|
260
|
+
|
|
261
|
+
expect(() => {
|
|
262
|
+
getPresence(instance, {resource: datasetResource})
|
|
263
|
+
}).not.toThrow()
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should work with a canvas resource', () => {
|
|
267
|
+
const canvasResource = {canvasId: 'canvas123'}
|
|
268
|
+
|
|
269
|
+
expect(() => {
|
|
270
|
+
getPresence(instance, {resource: canvasResource})
|
|
271
|
+
}).not.toThrow()
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('creates a project-hostname client for dataset resources', () => {
|
|
275
|
+
getPresence(instance, {resource: {projectId: 'my-project', dataset: 'my-dataset'}})
|
|
276
|
+
|
|
277
|
+
expect(getClient).toHaveBeenCalledWith(instance, {
|
|
278
|
+
apiVersion: '2026-03-30',
|
|
279
|
+
projectId: 'my-project',
|
|
280
|
+
dataset: 'my-dataset',
|
|
281
|
+
useProjectHostname: true,
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('creates a resource client for canvas resources', () => {
|
|
286
|
+
const canvasResource = {canvasId: 'canvas123'}
|
|
287
|
+
getPresence(instance, {resource: canvasResource})
|
|
288
|
+
|
|
289
|
+
expect(getClient).toHaveBeenCalledWith(instance, {
|
|
290
|
+
apiVersion: '2026-03-30',
|
|
291
|
+
resource: canvasResource,
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('fetches organizationId from canvas endpoint for canvas resources', () => {
|
|
296
|
+
const canvasResource = {canvasId: 'canvas123'}
|
|
297
|
+
getPresence(instance, {resource: canvasResource})
|
|
298
|
+
|
|
299
|
+
expect(mockClient.observable.request).toHaveBeenCalledWith({
|
|
300
|
+
uri: '/canvases/canvas123',
|
|
301
|
+
tag: 'canvases.get',
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('does not fetch organizationId for dataset resources', () => {
|
|
306
|
+
getPresence(instance, {resource: {projectId: 'my-project', dataset: 'my-dataset'}})
|
|
307
|
+
|
|
308
|
+
expect(mockClient.observable.request).not.toHaveBeenCalled()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('fetches user data for canvas users', async () => {
|
|
312
|
+
const source = getPresence(instance, {resource: {canvasId: 'canvas123'}})
|
|
313
|
+
const unsubscribe = source.subscribe(() => {})
|
|
314
|
+
|
|
315
|
+
await firstValueFrom(of(null).pipe(delay(10)))
|
|
316
|
+
|
|
317
|
+
mockIncomingEvents.next({
|
|
318
|
+
type: 'state',
|
|
319
|
+
userId: 'user-1',
|
|
320
|
+
sessionId: 'other-session',
|
|
321
|
+
timestamp: '2023-01-01T12:00:00Z',
|
|
322
|
+
locations: [
|
|
323
|
+
{
|
|
324
|
+
type: 'document',
|
|
325
|
+
documentId: 'doc-1',
|
|
326
|
+
path: ['title'],
|
|
327
|
+
lastActiveAt: '2023-01-01T12:00:00Z',
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
await firstValueFrom(of(null).pipe(delay(50)))
|
|
333
|
+
|
|
334
|
+
expect(getUserState).toHaveBeenCalledWith(instance, {
|
|
335
|
+
userId: 'user-1',
|
|
336
|
+
resourceType: 'organization',
|
|
337
|
+
organizationId: 'test-org-id',
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
unsubscribe()
|
|
341
|
+
})
|
|
246
342
|
})
|
|
247
343
|
})
|