@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.1
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/createGroqSearchFilter.d.ts +1147 -0
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
- package/dist/_chunks-es/_internal.js +134 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1864 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +93 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_exports/_internal.d.ts +95 -0
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/_exports/_internal.js +24 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +1079 -1076
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1967 -2302
- package/dist/index.js.map +1 -1
- package/package.json +48 -47
- package/src/_exports/_internal.ts +26 -0
- package/src/_exports/index.ts +120 -11
- package/src/access/checkPermissions.test.ts +55 -0
- package/src/access/checkPermissions.ts +54 -0
- package/src/agent/agentActions.test.ts +16 -60
- package/src/agent/agentActions.ts +25 -38
- package/src/applications/applications.test-d.ts +83 -0
- package/src/applications/applications.test.ts +130 -0
- package/src/applications/applications.ts +364 -0
- package/src/applications/userApplications.test.ts +126 -0
- package/src/applications/userApplications.ts +261 -0
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authMode.test.ts +25 -0
- package/src/auth/authMode.ts +6 -3
- package/src/auth/authStore.test.ts +194 -13
- package/src/auth/authStore.ts +59 -26
- package/src/auth/getOrganizationVerificationState.test.ts +18 -17
- package/src/auth/getOrganizationVerificationState.ts +4 -4
- package/src/auth/handleAuthCallback.test.ts +35 -1
- package/src/auth/handleAuthCallback.ts +25 -6
- package/src/auth/logout.test.ts +75 -2
- package/src/auth/logout.ts +23 -4
- package/src/auth/refreshStampedToken.test.ts +219 -469
- package/src/auth/refreshStampedToken.ts +79 -195
- package/src/auth/studioAuth.ts +1 -1
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
- package/src/client/clientStore.test.ts +62 -37
- package/src/client/clientStore.ts +45 -75
- package/src/client/liveEvents.test.ts +151 -0
- package/src/client/liveEvents.ts +107 -0
- package/src/comlink/controller/actions/destroyController.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
- package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
- package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
- package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/actions/releaseNode.test.ts +7 -4
- package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
- package/src/comlink/node/getNodeState.test.ts +1 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/__tests__/handles.test.ts +18 -12
- package/src/config/handles.ts +25 -7
- package/src/config/sanityConfig.ts +109 -79
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +10 -4
- package/src/document/actions.test.ts +115 -33
- package/src/document/actions.ts +212 -15
- package/src/document/applyDocumentActions.test.ts +38 -12
- package/src/document/applyDocumentActions.ts +30 -9
- package/src/document/documentConstants.ts +7 -0
- package/src/document/documentStore.concurrency.test.ts +1306 -0
- package/src/document/documentStore.test.ts +673 -278
- package/src/document/documentStore.ts +162 -71
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +82 -26
- package/src/document/listen.test.ts +56 -0
- package/src/document/listen.ts +74 -21
- package/src/document/listenerEventOperators.test.ts +311 -0
- package/src/document/listenerEventOperators.ts +217 -0
- package/src/document/patchOperations.test.ts +49 -2
- package/src/document/patchOperations.ts +50 -0
- package/src/document/permissions.test.ts +105 -60
- package/src/document/permissions.ts +44 -19
- package/src/document/processActions/create.ts +138 -0
- package/src/document/processActions/delete.ts +100 -0
- package/src/document/processActions/discard.ts +63 -0
- package/src/document/processActions/edit.ts +154 -0
- package/src/document/processActions/processActions.ts +218 -0
- package/src/document/processActions/publish.ts +120 -0
- package/src/document/processActions/releaseArchive.ts +77 -0
- package/src/document/processActions/releaseCreate.ts +59 -0
- package/src/document/processActions/releaseDelete.ts +65 -0
- package/src/document/processActions/releaseEdit.ts +37 -0
- package/src/document/processActions/releasePublish.ts +45 -0
- package/src/document/processActions/releaseSchedule.ts +87 -0
- package/src/document/processActions/releaseUtil.ts +31 -0
- package/src/document/processActions/shared.ts +308 -0
- package/src/document/processActions/unpublish.ts +85 -0
- package/src/document/processActions.test.ts +1129 -121
- package/src/document/reducers.test.ts +639 -64
- package/src/document/reducers.ts +187 -48
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- package/src/document/sharedListener.test.ts +16 -14
- package/src/document/sharedListener.ts +24 -8
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +5 -6
- package/src/installations/installations.test-d.ts +68 -0
- package/src/installations/installations.test.ts +62 -0
- package/src/installations/installations.ts +202 -0
- package/src/organization/organization.test-d.ts +36 -0
- package/src/organization/organization.test.ts +138 -0
- package/src/organization/organization.ts +138 -0
- package/src/organizations/organizations.test-d.ts +56 -0
- package/src/organizations/organizations.test.ts +150 -0
- package/src/organizations/organizations.ts +107 -0
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +19 -2
- package/src/presence/presenceStore.test.ts +94 -23
- package/src/presence/presenceStore.ts +85 -29
- package/src/preview/getPreviewState.test.ts +137 -0
- package/src/preview/getPreviewState.ts +69 -0
- package/src/preview/previewProjectionUtils.test.ts +9 -22
- package/src/preview/previewProjectionUtils.ts +7 -3
- package/src/preview/resolvePreview.test.ts +64 -0
- package/src/preview/resolvePreview.ts +43 -0
- package/src/preview/types.ts +10 -0
- package/src/project/project.test-d.ts +37 -0
- package/src/project/project.test.ts +157 -12
- package/src/project/project.ts +138 -25
- package/src/projection/getProjectionState.test.ts +18 -20
- package/src/projection/getProjectionState.ts +23 -3
- package/src/projection/projectionQuery.ts +5 -5
- package/src/projection/projectionStore.test.ts +1 -1
- package/src/projection/resolveProjection.test.ts +1 -2
- package/src/projection/resolveProjection.ts +16 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
- package/src/projection/types.ts +8 -0
- package/src/projects/projects.test-d.ts +32 -0
- package/src/projects/projects.test.ts +108 -42
- package/src/projects/projects.ts +63 -27
- package/src/query/queryStore.test.ts +259 -123
- package/src/query/queryStore.ts +78 -63
- package/src/query/reducers.test.ts +0 -42
- package/src/query/reducers.ts +2 -18
- package/src/releases/getPerspectiveState.test.ts +17 -23
- package/src/releases/getPerspectiveState.ts +8 -20
- package/src/releases/observeReleases.test.ts +127 -0
- package/src/releases/observeReleases.ts +102 -0
- package/src/releases/releasesStore.test.ts +100 -53
- package/src/releases/releasesStore.ts +69 -44
- package/src/releases/utils/sortReleases.test.ts +38 -2
- package/src/releases/utils/sortReleases.ts +15 -10
- package/src/store/createActionBinder.test.ts +87 -90
- package/src/store/createActionBinder.ts +79 -25
- package/src/store/createSanityInstance.test.ts +83 -13
- package/src/store/createSanityInstance.ts +77 -2
- package/src/store/createStateSourceAction.test.ts +64 -2
- package/src/store/createStateSourceAction.ts +39 -44
- package/src/store/createStoreInstance.test.ts +4 -2
- package/src/store/fetcherStore.test-d.ts +121 -0
- package/src/store/fetcherStore.test.ts +299 -0
- package/src/store/fetcherStore.ts +801 -0
- package/src/telemetry/environment.test.ts +119 -0
- package/src/telemetry/environment.ts +92 -0
- package/src/telemetry/events.ts +42 -0
- package/src/telemetry/initTelemetry.test.ts +432 -0
- package/src/telemetry/initTelemetry.ts +218 -0
- package/src/telemetry/telemetryManager.test.ts +327 -0
- package/src/telemetry/telemetryManager.ts +199 -0
- package/src/users/reducers.test.ts +6 -1
- package/src/users/reducers.ts +5 -6
- package/src/users/types.ts +4 -4
- package/src/users/usersStore.test.ts +16 -12
- package/src/users/usersStore.ts +5 -1
- package/src/utils/buildQuery.test.ts +14 -0
- package/src/utils/buildQuery.ts +17 -0
- package/src/utils/createFetcherStore.test.ts +7 -5
- 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/includeShape.ts +36 -0
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/logger.test.ts +12 -0
- package/src/utils/logger.ts +8 -3
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
- package/src/utils/setCleanupTimeout.ts +37 -0
- package/src/document/processActions.ts +0 -707
- package/src/utils/defineIntent.test.ts +0 -477
- package/src/utils/defineIntent.ts +0 -244
package/src/document/reducers.ts
CHANGED
|
@@ -1,31 +1,54 @@
|
|
|
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 {
|
|
7
|
+
import {insecureRandomId} from '../utils/ids'
|
|
8
|
+
import {isDeepEqual, omitProperty} from '../utils/object'
|
|
9
|
+
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
10
|
+
import {type Action} from './actions'
|
|
8
11
|
import {DOCUMENT_STATE_CLEAR_DELAY} from './documentConstants'
|
|
9
12
|
import {type DocumentState, type DocumentStoreState} from './documentStore'
|
|
10
13
|
import {type RemoteDocument} from './listen'
|
|
11
|
-
import {ActionError, processActions} from './processActions'
|
|
14
|
+
import {ActionError, processActions} from './processActions/processActions'
|
|
15
|
+
import {getReleaseDocumentId, isReleaseAction} from './processActions/releaseUtil'
|
|
12
16
|
import {type DocumentSet} from './processMutations'
|
|
13
17
|
|
|
14
18
|
const EMPTY_REVISIONS: NonNullable<Required<DocumentState['unverifiedRevisions']>> = {}
|
|
15
19
|
|
|
20
|
+
/**
|
|
21
|
+
* How many of this client's own transaction IDs to remember per document for
|
|
22
|
+
* `remote-patches` origin labeling. Sized to comfortably outlast the window
|
|
23
|
+
* between submitting a transaction and observing its listener echo.
|
|
24
|
+
*/
|
|
25
|
+
const MAX_RECENT_OWN_TRANSACTION_IDS = 50
|
|
26
|
+
|
|
16
27
|
export type SyncTransactionState = Pick<
|
|
17
28
|
DocumentStoreState,
|
|
18
|
-
'queued' | 'applied' | 'documentStates' | 'outgoing' | 'grants'
|
|
29
|
+
'queued' | 'applied' | 'documentStates' | 'outgoing' | 'grants' | 'identity'
|
|
19
30
|
>
|
|
20
31
|
|
|
32
|
+
type DocumentHandleLike = Pick<DocumentHandle, 'perspective'> & {
|
|
33
|
+
documentId?: string
|
|
34
|
+
liveEdit?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
type ActionMap = {
|
|
22
38
|
create: 'sanity.action.document.version.create'
|
|
23
|
-
createLiveEdit: 'sanity.action.document.create'
|
|
24
39
|
discard: 'sanity.action.document.version.discard'
|
|
25
40
|
unpublish: 'sanity.action.document.unpublish'
|
|
26
41
|
delete: 'sanity.action.document.delete'
|
|
27
42
|
edit: 'sanity.action.document.edit'
|
|
28
43
|
publish: 'sanity.action.document.publish'
|
|
44
|
+
releaseCreate: 'sanity.action.release.create'
|
|
45
|
+
releaseEdit: 'sanity.action.release.edit'
|
|
46
|
+
releasePublish: 'sanity.action.release.publish'
|
|
47
|
+
releaseSchedule: 'sanity.action.release.schedule'
|
|
48
|
+
releaseUnschedule: 'sanity.action.release.unschedule'
|
|
49
|
+
releaseArchive: 'sanity.action.release.archive'
|
|
50
|
+
releaseUnarchive: 'sanity.action.release.unarchive'
|
|
51
|
+
releaseDelete: 'sanity.action.release.delete'
|
|
29
52
|
}
|
|
30
53
|
|
|
31
54
|
type OptimisticLock = {
|
|
@@ -33,14 +56,33 @@ type OptimisticLock = {
|
|
|
33
56
|
ifPublishedRevisionId?: string
|
|
34
57
|
}
|
|
35
58
|
|
|
59
|
+
interface ReleaseMetadataPayload {
|
|
60
|
+
title?: string
|
|
61
|
+
description?: string
|
|
62
|
+
intendedPublishAt?: string
|
|
63
|
+
releaseType?: 'asap' | 'scheduled' | 'undecided'
|
|
64
|
+
cardinality?: 'one' | 'many'
|
|
65
|
+
}
|
|
66
|
+
|
|
36
67
|
export type HttpAction =
|
|
37
68
|
| {actionType: ActionMap['create']; publishedId: string; attributes: SanityDocumentLike}
|
|
38
|
-
| {actionType: ActionMap['createLiveEdit']; publishedId: string; attributes: SanityDocumentLike}
|
|
39
69
|
| {actionType: ActionMap['discard']; versionId: string; purge?: boolean}
|
|
40
70
|
| {actionType: ActionMap['unpublish']; draftId: string; publishedId: string}
|
|
41
71
|
| {actionType: ActionMap['delete']; publishedId: string; includeDrafts?: string[]}
|
|
42
72
|
| {actionType: ActionMap['edit']; draftId: string; publishedId: string; patch: PatchOperations}
|
|
43
73
|
| ({actionType: ActionMap['publish']; draftId: string; publishedId: string} & OptimisticLock)
|
|
74
|
+
| {
|
|
75
|
+
actionType: ActionMap['releaseCreate']
|
|
76
|
+
releaseId: string
|
|
77
|
+
metadata?: ReleaseMetadataPayload
|
|
78
|
+
}
|
|
79
|
+
| {actionType: ActionMap['releaseEdit']; releaseId: string; patch: PatchOperations}
|
|
80
|
+
| {actionType: ActionMap['releasePublish']; releaseId: string}
|
|
81
|
+
| {actionType: ActionMap['releaseSchedule']; releaseId: string; publishAt: string}
|
|
82
|
+
| {actionType: ActionMap['releaseUnschedule']; releaseId: string}
|
|
83
|
+
| {actionType: ActionMap['releaseArchive']; releaseId: string}
|
|
84
|
+
| {actionType: ActionMap['releaseUnarchive']; releaseId: string}
|
|
85
|
+
| {actionType: ActionMap['releaseDelete']; releaseId: string}
|
|
44
86
|
|
|
45
87
|
/**
|
|
46
88
|
* Represents a transaction that is queued to be applied but has not yet been
|
|
@@ -57,7 +99,7 @@ export interface QueuedTransaction {
|
|
|
57
99
|
* actions don't mention draft IDs and is meant to abstract away the draft
|
|
58
100
|
* model from users.
|
|
59
101
|
*/
|
|
60
|
-
actions:
|
|
102
|
+
actions: Action[]
|
|
61
103
|
/**
|
|
62
104
|
* An optional flag set to disable this transaction from being batched with
|
|
63
105
|
* other transactions.
|
|
@@ -115,9 +157,9 @@ export interface AppliedTransaction extends QueuedTransaction {
|
|
|
115
157
|
outgoingActions: HttpAction[]
|
|
116
158
|
|
|
117
159
|
/**
|
|
118
|
-
* similar to `outgoingActions` but comprised of mutations instead of
|
|
119
|
-
*
|
|
120
|
-
*
|
|
160
|
+
* similar to `outgoingActions` but comprised of mutations instead of actions.
|
|
161
|
+
* Useful for debugging, and is also used by liveEdit documents to send mutations,
|
|
162
|
+
* since they can't use the Actions API which is pretty dependent on the draft model.
|
|
121
163
|
*/
|
|
122
164
|
outgoingMutations: Mutation[]
|
|
123
165
|
}
|
|
@@ -145,7 +187,7 @@ export function queueTransaction(
|
|
|
145
187
|
transaction: QueuedTransaction,
|
|
146
188
|
): SyncTransactionState {
|
|
147
189
|
const {transactionId, actions} = transaction
|
|
148
|
-
const prevWithSubscriptionIds =
|
|
190
|
+
const prevWithSubscriptionIds = getDocumentIdsFromHandleLikes(actions).reduce(
|
|
149
191
|
(acc, id) => addSubscriptionIdToDocument(acc, id, transactionId),
|
|
150
192
|
prev,
|
|
151
193
|
)
|
|
@@ -163,7 +205,7 @@ export function removeQueuedTransaction(
|
|
|
163
205
|
const transaction = prev.queued.find((t) => t.transactionId === transactionId)
|
|
164
206
|
if (!transaction) return prev
|
|
165
207
|
|
|
166
|
-
const prevWithSubscriptionIds =
|
|
208
|
+
const prevWithSubscriptionIds = getDocumentIdsFromHandleLikes(transaction.actions).reduce(
|
|
167
209
|
(acc, id) => removeSubscriptionIdFromDocument(acc, id, transactionId),
|
|
168
210
|
prev,
|
|
169
211
|
)
|
|
@@ -179,7 +221,7 @@ export function applyFirstQueuedTransaction(prev: SyncTransactionState): SyncTra
|
|
|
179
221
|
if (!queued) return prev
|
|
180
222
|
if (!prev.grants) return prev
|
|
181
223
|
|
|
182
|
-
const ids =
|
|
224
|
+
const ids = getDocumentIdsFromHandleLikes(queued.actions)
|
|
183
225
|
// the local value is only ever `undefined` if it has not been loaded yet
|
|
184
226
|
// we can't get the next applied state unless all relevant documents are ready
|
|
185
227
|
if (ids.some((id) => prev.documentStates[id]?.local === undefined)) return prev
|
|
@@ -197,6 +239,7 @@ export function applyFirstQueuedTransaction(prev: SyncTransactionState): SyncTra
|
|
|
197
239
|
base: working,
|
|
198
240
|
timestamp,
|
|
199
241
|
grants: prev.grants,
|
|
242
|
+
identity: prev.identity,
|
|
200
243
|
})
|
|
201
244
|
const applied: AppliedTransaction = {
|
|
202
245
|
...queued,
|
|
@@ -265,6 +308,11 @@ export function batchAppliedTransactions([curr, ...rest]: AppliedTransaction[]):
|
|
|
265
308
|
if (!next) return undefined
|
|
266
309
|
if (next.disableBatching) return editAction
|
|
267
310
|
|
|
311
|
+
// Don't batch a liveEdit edit with a non-liveEdit edit — they route to different APIs
|
|
312
|
+
const nextFirst = next.actions[0]
|
|
313
|
+
const nextLiveEdit = nextFirst && 'liveEdit' in nextFirst ? nextFirst.liveEdit : false
|
|
314
|
+
if (!!action.liveEdit !== !!nextLiveEdit) return editAction
|
|
315
|
+
|
|
268
316
|
return {
|
|
269
317
|
disableBatching: false,
|
|
270
318
|
// Use the transactionId from the later (next) transaction.
|
|
@@ -323,6 +371,15 @@ export function transitionAppliedTransactionsToOutgoing(
|
|
|
323
371
|
// add unverified revision
|
|
324
372
|
[transactionId]: {documentId, previousRev, transactionId, timestamp},
|
|
325
373
|
},
|
|
374
|
+
// also track the transaction ID durably (unverified revisions can be
|
|
375
|
+
// pruned by sync events before the listener echo arrives) so
|
|
376
|
+
// `remote-patches` events label our own transactions correctly
|
|
377
|
+
recentOwnTransactionIds: [
|
|
378
|
+
...(documentState.recentOwnTransactionIds ?? []).slice(
|
|
379
|
+
-(MAX_RECENT_OWN_TRANSACTION_IDS - 1),
|
|
380
|
+
),
|
|
381
|
+
transactionId,
|
|
382
|
+
],
|
|
326
383
|
}
|
|
327
384
|
|
|
328
385
|
return acc
|
|
@@ -337,7 +394,7 @@ export function cleanupOutgoingTransaction(prev: SyncTransactionState): SyncTran
|
|
|
337
394
|
if (!outgoing) return prev
|
|
338
395
|
|
|
339
396
|
let next = prev
|
|
340
|
-
const ids =
|
|
397
|
+
const ids = getDocumentIdsFromHandleLikes(outgoing.actions)
|
|
341
398
|
for (const transactionId of outgoing.batchedTransactionIds) {
|
|
342
399
|
for (const documentId of ids) {
|
|
343
400
|
next = removeSubscriptionIdFromDocument(next, documentId, transactionId)
|
|
@@ -359,7 +416,7 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
|
|
|
359
416
|
|
|
360
417
|
for (const t of prev.applied) {
|
|
361
418
|
try {
|
|
362
|
-
const next = processActions({...t, working, grants: prev.grants})
|
|
419
|
+
const next = processActions({...t, working, grants: prev.grants, identity: prev.identity})
|
|
363
420
|
working = next.working
|
|
364
421
|
nextApplied.push({...t, ...next})
|
|
365
422
|
} catch (error) {
|
|
@@ -383,7 +440,7 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
|
|
|
383
440
|
local: documentId in working ? working[documentId] : local,
|
|
384
441
|
unverifiedRevisions:
|
|
385
442
|
prev.outgoing && prev.outgoing.transactionId in unverifiedRevisions
|
|
386
|
-
?
|
|
443
|
+
? omitProperty(unverifiedRevisions, prev.outgoing.transactionId)
|
|
387
444
|
: unverifiedRevisions,
|
|
388
445
|
}
|
|
389
446
|
return [documentId, next] as const
|
|
@@ -392,9 +449,29 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
|
|
|
392
449
|
}
|
|
393
450
|
}
|
|
394
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Extracts the patch operations addressed to the given document from a set of
|
|
454
|
+
* raw listener mutations, stripping the mutation-level `id` addressing so the
|
|
455
|
+
* patches are rooted at the document. A transaction can carry mutations for
|
|
456
|
+
* other documents, so anything not explicitly id-addressed to this document
|
|
457
|
+
* (including query-addressed patches) is dropped.
|
|
458
|
+
*/
|
|
459
|
+
function extractPatchOperations(
|
|
460
|
+
mutations: Mutation[] | undefined,
|
|
461
|
+
documentId: string,
|
|
462
|
+
): PatchOperations[] {
|
|
463
|
+
if (!mutations) return []
|
|
464
|
+
return mutations.flatMap((mutation) => {
|
|
465
|
+
if (!('patch' in mutation) || !mutation.patch) return []
|
|
466
|
+
const {id, ...operations} = mutation.patch as PatchOperations & {id?: string}
|
|
467
|
+
if (id !== documentId) return []
|
|
468
|
+
return [operations]
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
|
|
395
472
|
export function applyRemoteDocument(
|
|
396
473
|
prev: SyncTransactionState,
|
|
397
|
-
{document, documentId, previousRev, revision, timestamp, type}: RemoteDocument,
|
|
474
|
+
{document, documentId, previousRev, revision, timestamp, type, mutations}: RemoteDocument,
|
|
398
475
|
events: DocumentStoreState['events'],
|
|
399
476
|
): SyncTransactionState {
|
|
400
477
|
if (!prev.grants) return prev
|
|
@@ -411,7 +488,32 @@ export function applyRemoteDocument(
|
|
|
411
488
|
const revisionToVerify = revision ? prevUnverifiedRevisions?.[revision] : undefined
|
|
412
489
|
let unverifiedRevisions = prevUnverifiedRevisions ?? EMPTY_REVISIONS
|
|
413
490
|
if (revision && revisionToVerify) {
|
|
414
|
-
unverifiedRevisions =
|
|
491
|
+
unverifiedRevisions = omitProperty(prevUnverifiedRevisions, revision)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// surface the operational patches from this transaction before they are
|
|
495
|
+
// collapsed into the whole-document snapshot below. consumers that keep
|
|
496
|
+
// their own state (e.g. collaborative text editors) rely on these to apply
|
|
497
|
+
// remote changes without re-diffing document snapshots
|
|
498
|
+
if (type === 'mutation' && revision) {
|
|
499
|
+
const patches = extractPatchOperations(mutations, documentId)
|
|
500
|
+
if (patches.length) {
|
|
501
|
+
// `unverifiedRevisions` alone isn't a reliable origin marker: a sync
|
|
502
|
+
// event can prune an in-flight transaction's entry before its listener
|
|
503
|
+
// echo arrives. `recentOwnTransactionIds` survives that race
|
|
504
|
+
const isOwnTransaction =
|
|
505
|
+
Boolean(revisionToVerify) ||
|
|
506
|
+
(prevDocState.recentOwnTransactionIds?.includes(revision) ?? false)
|
|
507
|
+
events.next({
|
|
508
|
+
type: 'remote-patches',
|
|
509
|
+
documentId,
|
|
510
|
+
transactionId: revision,
|
|
511
|
+
previousRev,
|
|
512
|
+
timestamp,
|
|
513
|
+
patches,
|
|
514
|
+
origin: isOwnTransaction ? 'local' : 'remote',
|
|
515
|
+
})
|
|
516
|
+
}
|
|
415
517
|
}
|
|
416
518
|
|
|
417
519
|
// if this remote document is from a `'sync'` event (meaning that the whole
|
|
@@ -432,6 +534,34 @@ export function applyRemoteDocument(
|
|
|
432
534
|
// matches the previous revision we expected, we can "fast-forward" and skip
|
|
433
535
|
// rebasing local changes on top of this new base
|
|
434
536
|
if (revisionToVerify && revisionToVerify.previousRev === previousRev) {
|
|
537
|
+
// even on a clean fast-forward, the server's result can differ from our
|
|
538
|
+
// optimistic prediction: a publish derives the published content from the
|
|
539
|
+
// server's copy of the draft, which may carry concurrent edits our
|
|
540
|
+
// prediction never saw. once nothing else is pending locally for this
|
|
541
|
+
// document (no applied transaction and no in-flight transaction, other
|
|
542
|
+
// than the one this event just confirmed, modifies it), converge `local`
|
|
543
|
+
// to the server's document. while local work that modifies this document
|
|
544
|
+
// is pending, keep the optimistic `local` so those changes stay visible;
|
|
545
|
+
// that work's own echo (or rebase) recomputes `local` later.
|
|
546
|
+
//
|
|
547
|
+
// "modifies" matters: pending work on other documents must not block
|
|
548
|
+
// convergence here (nothing would ever revisit this document's `local`),
|
|
549
|
+
// and a pending draft edit carries the published document in its working
|
|
550
|
+
// set without changing it, so a presence check would block published-doc
|
|
551
|
+
// convergence whenever a draft edit is pending. this is the same
|
|
552
|
+
// modified-test `transitionAppliedTransactionsToOutgoing` uses
|
|
553
|
+
const modifiesDocument = (transaction: AppliedTransaction) =>
|
|
554
|
+
transaction.working[documentId]?._rev !== transaction.previousRevs[documentId]
|
|
555
|
+
const nothingElsePending =
|
|
556
|
+
!prev.applied.some(modifiesDocument) &&
|
|
557
|
+
(!prev.outgoing ||
|
|
558
|
+
prev.outgoing.transactionId === revision ||
|
|
559
|
+
!modifiesDocument(prev.outgoing))
|
|
560
|
+
const local =
|
|
561
|
+
nothingElsePending && !isDeepEqual(prevDocState.local, document)
|
|
562
|
+
? document
|
|
563
|
+
: prevDocState.local
|
|
564
|
+
|
|
435
565
|
return {
|
|
436
566
|
...prev,
|
|
437
567
|
documentStates: {
|
|
@@ -440,6 +570,7 @@ export function applyRemoteDocument(
|
|
|
440
570
|
...prevDocState,
|
|
441
571
|
remote: document,
|
|
442
572
|
remoteRev: revision,
|
|
573
|
+
local,
|
|
443
574
|
unverifiedRevisions,
|
|
444
575
|
},
|
|
445
576
|
},
|
|
@@ -466,7 +597,7 @@ export function applyRemoteDocument(
|
|
|
466
597
|
// transaction again through the listener and this same flow will run then
|
|
467
598
|
for (const curr of prev.applied) {
|
|
468
599
|
try {
|
|
469
|
-
const next = processActions({...curr, working, grants: prev.grants})
|
|
600
|
+
const next = processActions({...curr, working, grants: prev.grants, identity: prev.identity})
|
|
470
601
|
working = next.working
|
|
471
602
|
// next includes an updated `previous` set and `working` set and updates
|
|
472
603
|
// the `outgoingAction` and `outgoingMutations`. the `base` set from the
|
|
@@ -490,6 +621,12 @@ export function applyRemoteDocument(
|
|
|
490
621
|
}
|
|
491
622
|
}
|
|
492
623
|
|
|
624
|
+
// when the recompute lands on a value deep-equal to the current local
|
|
625
|
+
// (the common case for echoes of our own transactions), keep the previous
|
|
626
|
+
// reference so subscribers don't re-render for an identical value
|
|
627
|
+
const nextLocal = working[documentId]
|
|
628
|
+
const local = isDeepEqual(prevDocState.local, nextLocal) ? prevDocState.local : nextLocal
|
|
629
|
+
|
|
493
630
|
return {
|
|
494
631
|
...prev,
|
|
495
632
|
applied: nextApplied,
|
|
@@ -499,7 +636,7 @@ export function applyRemoteDocument(
|
|
|
499
636
|
...prevDocState,
|
|
500
637
|
remote: document,
|
|
501
638
|
remoteRev: revision,
|
|
502
|
-
local
|
|
639
|
+
local,
|
|
503
640
|
unverifiedRevisions,
|
|
504
641
|
},
|
|
505
642
|
},
|
|
@@ -538,7 +675,7 @@ export function removeSubscriptionIdFromDocument(
|
|
|
538
675
|
|
|
539
676
|
if (!prevDocState) return prev
|
|
540
677
|
if (!subscriptions.length) {
|
|
541
|
-
return {...prev, documentStates:
|
|
678
|
+
return {...prev, documentStates: omitProperty(prev.documentStates, documentId)}
|
|
542
679
|
}
|
|
543
680
|
return {
|
|
544
681
|
...prev,
|
|
@@ -551,22 +688,10 @@ export function removeSubscriptionIdFromDocument(
|
|
|
551
688
|
|
|
552
689
|
export function manageSubscriberIds(
|
|
553
690
|
{state}: StoreContext<SyncTransactionState>,
|
|
554
|
-
|
|
555
|
-
options?: {expandDraftPublished?: boolean},
|
|
691
|
+
handles: DocumentHandleLike[],
|
|
556
692
|
): () => 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
|
-
)
|
|
693
|
+
const documentIds = getDocumentIdsFromHandleLikes(handles)
|
|
694
|
+
|
|
570
695
|
const subscriptionId = insecureRandomId()
|
|
571
696
|
state.set('addSubscribers', (prev) =>
|
|
572
697
|
documentIds.reduce(
|
|
@@ -576,7 +701,7 @@ export function manageSubscriberIds(
|
|
|
576
701
|
)
|
|
577
702
|
|
|
578
703
|
return () => {
|
|
579
|
-
|
|
704
|
+
setCleanupTimeout(() => {
|
|
580
705
|
state.set('removeSubscribers', (prev) =>
|
|
581
706
|
documentIds.reduce(
|
|
582
707
|
(acc, id) => removeSubscriptionIdFromDocument(acc, id, subscriptionId),
|
|
@@ -587,13 +712,27 @@ export function manageSubscriberIds(
|
|
|
587
712
|
}
|
|
588
713
|
}
|
|
589
714
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
715
|
+
// document handles are passed in via the public facing API, but we also need to
|
|
716
|
+
// pull the correct document ids from action bodies, which have similar but not
|
|
717
|
+
// identical shapes to the document handles. release actions also flow through
|
|
718
|
+
// here, and resolve to the underlying release document id.
|
|
719
|
+
function getDocumentIdsFromHandleLikes(handles: (DocumentHandleLike | Action)[]): string[] {
|
|
720
|
+
return handles.flatMap((handle) => {
|
|
721
|
+
if ('type' in handle && isReleaseAction(handle)) {
|
|
722
|
+
return [getReleaseDocumentId(handle.releaseId)]
|
|
723
|
+
}
|
|
724
|
+
const idsForDocument = []
|
|
725
|
+
if (!handle.documentId) return []
|
|
726
|
+
if (handle.liveEdit) {
|
|
727
|
+
return [handle.documentId]
|
|
728
|
+
}
|
|
729
|
+
if (isReleasePerspective(handle.perspective)) {
|
|
730
|
+
idsForDocument.push(
|
|
731
|
+
getVersionId(DocumentId(handle.documentId), handle.perspective.releaseName),
|
|
732
|
+
)
|
|
733
|
+
}
|
|
734
|
+
idsForDocument.push(getPublishedId(DocumentId(handle.documentId)))
|
|
735
|
+
idsForDocument.push(getDraftId(DocumentId(handle.documentId)))
|
|
736
|
+
return idsForDocument
|
|
737
|
+
})
|
|
599
738
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {type EditDocumentAction} from './actions'
|
|
4
|
+
import {getEffectiveDocumentModel, normalizeActionsForResource} from './resourceRules'
|
|
5
|
+
|
|
6
|
+
const datasetResource = {projectId: 'p', dataset: 'd'}
|
|
7
|
+
const canvasResource = {canvasId: 'canvas-1'}
|
|
8
|
+
const mediaLibraryResource = {mediaLibraryId: 'ml-1'}
|
|
9
|
+
|
|
10
|
+
function editAction(
|
|
11
|
+
overrides: Partial<EditDocumentAction> & Pick<EditDocumentAction, 'documentId' | 'documentType'>,
|
|
12
|
+
): EditDocumentAction {
|
|
13
|
+
return {
|
|
14
|
+
type: 'document.edit',
|
|
15
|
+
patches: [{set: {foo: 'bar'}}],
|
|
16
|
+
...overrides,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('getEffectiveDocumentModel', () => {
|
|
21
|
+
it('returns passthrough for no resource', () => {
|
|
22
|
+
expect(getEffectiveDocumentModel(undefined, 'anything')).toEqual({
|
|
23
|
+
liveEdit: undefined,
|
|
24
|
+
supportsReleases: true,
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns passthrough for dataset resources', () => {
|
|
29
|
+
expect(getEffectiveDocumentModel(datasetResource, 'author')).toEqual({
|
|
30
|
+
liveEdit: undefined,
|
|
31
|
+
supportsReleases: true,
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('forces liveEdit and disallows releases for canvas resources', () => {
|
|
36
|
+
expect(getEffectiveDocumentModel(canvasResource, 'page')).toEqual({
|
|
37
|
+
liveEdit: true,
|
|
38
|
+
supportsReleases: false,
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('forces liveEdit for non-asset media library types', () => {
|
|
43
|
+
expect(getEffectiveDocumentModel(mediaLibraryResource, 'sanity.imageAsset')).toEqual({
|
|
44
|
+
liveEdit: true,
|
|
45
|
+
supportsReleases: false,
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('keeps draft/published model for sanity.asset in media library', () => {
|
|
50
|
+
expect(getEffectiveDocumentModel(mediaLibraryResource, 'sanity.asset')).toEqual({
|
|
51
|
+
liveEdit: false,
|
|
52
|
+
supportsReleases: false,
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('normalizeActionsForResource', () => {
|
|
58
|
+
let warnSpy: ReturnType<typeof vi.spyOn>
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
warnSpy.mockRestore()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('leaves dataset edits with a release perspective alone', () => {
|
|
69
|
+
const action = editAction({
|
|
70
|
+
documentId: 'versions.relA.doc1',
|
|
71
|
+
documentType: 'author',
|
|
72
|
+
perspective: {releaseName: 'relA'},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const result = normalizeActionsForResource([action], datasetResource)
|
|
76
|
+
|
|
77
|
+
expect(result[0]).toBe(action)
|
|
78
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('strips release perspective and forces liveEdit for canvas, warning once', () => {
|
|
82
|
+
const action = editAction({
|
|
83
|
+
documentId: 'versions.relA.doc1',
|
|
84
|
+
documentType: 'page',
|
|
85
|
+
perspective: {releaseName: 'relA'},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const [result] = normalizeActionsForResource([action], canvasResource) as [EditDocumentAction]
|
|
89
|
+
|
|
90
|
+
expect(result.liveEdit).toBe(true)
|
|
91
|
+
expect(result.perspective).toBeUndefined()
|
|
92
|
+
expect(result.documentId).toBe('doc1')
|
|
93
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
94
|
+
expect(warnSpy.mock.calls[0][0]).toContain('Canvas')
|
|
95
|
+
expect(warnSpy.mock.calls[0][0]).toContain('page (doc1)')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('strips release perspective and forces liveEdit for non-asset media library types', () => {
|
|
99
|
+
const action = editAction({
|
|
100
|
+
documentId: 'versions.relA.doc1',
|
|
101
|
+
documentType: 'sanity.imageAsset',
|
|
102
|
+
perspective: {releaseName: 'relA'},
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const [result] = normalizeActionsForResource([action], mediaLibraryResource) as [
|
|
106
|
+
EditDocumentAction,
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
expect(result.liveEdit).toBe(true)
|
|
110
|
+
expect(result.perspective).toBeUndefined()
|
|
111
|
+
expect(result.documentId).toBe('doc1')
|
|
112
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
113
|
+
expect(warnSpy.mock.calls[0][0]).toContain('Media Library')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('strips release perspective but keeps draft/publish model for sanity.asset', () => {
|
|
117
|
+
const action = editAction({
|
|
118
|
+
documentId: 'versions.relA.doc1',
|
|
119
|
+
documentType: 'sanity.asset',
|
|
120
|
+
perspective: {releaseName: 'relA'},
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const [result] = normalizeActionsForResource([action], mediaLibraryResource) as [
|
|
124
|
+
EditDocumentAction,
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
expect(result.liveEdit).toBeUndefined()
|
|
128
|
+
expect(result.perspective).toBeUndefined()
|
|
129
|
+
expect(result.documentId).toBe('doc1')
|
|
130
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('silently forces liveEdit for canvas with no release perspective', () => {
|
|
134
|
+
const action = editAction({
|
|
135
|
+
documentId: 'drafts.doc1',
|
|
136
|
+
documentType: 'page',
|
|
137
|
+
perspective: 'drafts',
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const [result] = normalizeActionsForResource([action], canvasResource) as [EditDocumentAction]
|
|
141
|
+
|
|
142
|
+
expect(result.liveEdit).toBe(true)
|
|
143
|
+
expect(result.documentId).toBe('doc1')
|
|
144
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('emits a single warning for multiple stripped actions in one call', () => {
|
|
148
|
+
const a = editAction({
|
|
149
|
+
documentId: 'versions.relA.doc1',
|
|
150
|
+
documentType: 'page',
|
|
151
|
+
perspective: {releaseName: 'relA'},
|
|
152
|
+
})
|
|
153
|
+
const b = editAction({
|
|
154
|
+
documentId: 'versions.relA.doc2',
|
|
155
|
+
documentType: 'page',
|
|
156
|
+
perspective: {releaseName: 'relA'},
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
normalizeActionsForResource([a, b], canvasResource)
|
|
160
|
+
|
|
161
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
162
|
+
expect(warnSpy.mock.calls[0][0]).toContain('doc1')
|
|
163
|
+
expect(warnSpy.mock.calls[0][0]).toContain('doc2')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('passes non-edit actions through unchanged', () => {
|
|
167
|
+
const action = {
|
|
168
|
+
type: 'document.publish' as const,
|
|
169
|
+
documentId: 'doc1',
|
|
170
|
+
documentType: 'page',
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = normalizeActionsForResource([action], canvasResource)
|
|
174
|
+
|
|
175
|
+
expect(result[0]).toBe(action)
|
|
176
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
177
|
+
})
|
|
178
|
+
})
|