@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.2
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 +163 -0
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/_exports/_internal.js +332 -0
- package/dist/_exports/_internal.js.map +1 -0
- 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-CtE9bLHW.js +754 -0
- package/dist/authStore-CtE9bLHW.js.map +1 -0
- package/dist/authStore-DKFX8fn7.d.ts +3179 -0
- package/dist/authStore-DKFX8fn7.d.ts.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 -2332
- package/dist/index.js +4171 -4332
- 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-DPf9sN3V.js +1346 -0
- package/dist/queryStore-DPf9sN3V.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-Dq_qCkeI.js +127 -0
- package/dist/telemetryManager-Dq_qCkeI.js.map +1 -0
- package/dist/version-BChjsi4u.js +9 -0
- package/dist/version-BChjsi4u.js.map +1 -0
- package/package.json +47 -52
- package/src/_exports/_internal.ts +27 -0
- package/src/_exports/agent.ts +19 -0
- package/src/_exports/comlink.ts +16 -0
- package/src/_exports/index.ts +125 -64
- 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 +88 -0
- package/src/applications/applications.test.ts +130 -0
- package/src/applications/applications.ts +367 -0
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authStore.test.ts +194 -13
- package/src/auth/authStore.ts +58 -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/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/__tests__/handles.test.ts +18 -12
- package/src/config/handles.ts +25 -7
- package/src/config/sanityConfig.ts +40 -82
- 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 +35 -13
- package/src/document/applyDocumentActions.ts +30 -9
- package/src/document/documentConstants.ts +15 -0
- package/src/document/documentStore.concurrency.test.ts +1306 -0
- package/src/document/documentStore.test.ts +753 -273
- package/src/document/documentStore.ts +197 -75
- 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 +76 -2
- package/src/document/patchOperations.ts +60 -1
- package/src/document/permissions.test.ts +107 -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 +772 -64
- package/src/document/reducers.ts +205 -51
- 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.test.ts +46 -0
- package/src/document/util.ts +50 -0
- package/src/favorites/favorites.test.ts +5 -6
- package/src/installations/installations.test-d.ts +73 -0
- package/src/installations/installations.test.ts +62 -0
- package/src/installations/installations.ts +206 -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 +201 -9
- package/src/presence/bifurTransport.ts +98 -14
- package/src/presence/paths.test.ts +70 -0
- package/src/presence/paths.ts +41 -0
- package/src/presence/presenceStore.test.ts +915 -24
- package/src/presence/presenceStore.ts +576 -104
- package/src/presence/types.ts +175 -3
- package/src/preview/previewProjectionUtils.test.ts +9 -22
- package/src/preview/previewProjectionUtils.ts +7 -3
- package/src/preview/types.ts +0 -15
- 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 -9
- 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/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 +85 -69
- 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 +109 -90
- package/src/store/createActionBinder.ts +36 -28
- package/src/store/createSanityInstance.test.ts +15 -13
- package/src/store/createSanityInstance.ts +14 -2
- package/src/store/createStateSourceAction.test.ts +64 -2
- package/src/store/createStateSourceAction.ts +38 -43
- 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 +3 -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)
|
|
@@ -349,8 +406,23 @@ export function cleanupOutgoingTransaction(prev: SyncTransactionState): SyncTran
|
|
|
349
406
|
|
|
350
407
|
export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTransactionState {
|
|
351
408
|
if (!prev.grants) return prev
|
|
409
|
+
|
|
410
|
+
// unregister the reverted batch's transaction-held subscriptions, symmetric
|
|
411
|
+
// with `cleanupOutgoingTransaction` on ack and `removeQueuedTransaction` on
|
|
412
|
+
// apply failure. without this, a reverted document's subscription IDs are
|
|
413
|
+
// never released and it can never be evicted
|
|
414
|
+
let base = prev
|
|
415
|
+
if (prev.outgoing) {
|
|
416
|
+
const ids = getDocumentIdsFromHandleLikes(prev.outgoing.actions)
|
|
417
|
+
for (const transactionId of prev.outgoing.batchedTransactionIds) {
|
|
418
|
+
for (const documentId of ids) {
|
|
419
|
+
base = removeSubscriptionIdFromDocument(base, documentId, transactionId)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
352
424
|
let working = Object.fromEntries(
|
|
353
|
-
Object.entries(
|
|
425
|
+
Object.entries(base.documentStates).map(([documentId, documentState]) => [
|
|
354
426
|
documentId,
|
|
355
427
|
documentState?.remote,
|
|
356
428
|
]),
|
|
@@ -359,7 +431,7 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
|
|
|
359
431
|
|
|
360
432
|
for (const t of prev.applied) {
|
|
361
433
|
try {
|
|
362
|
-
const next = processActions({...t, working, grants: prev.grants})
|
|
434
|
+
const next = processActions({...t, working, grants: prev.grants, identity: prev.identity})
|
|
363
435
|
working = next.working
|
|
364
436
|
nextApplied.push({...t, ...next})
|
|
365
437
|
} catch (error) {
|
|
@@ -371,11 +443,11 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
|
|
|
371
443
|
}
|
|
372
444
|
|
|
373
445
|
return {
|
|
374
|
-
...
|
|
446
|
+
...base,
|
|
375
447
|
applied: nextApplied,
|
|
376
448
|
outgoing: undefined,
|
|
377
449
|
documentStates: Object.fromEntries(
|
|
378
|
-
Object.entries(
|
|
450
|
+
Object.entries(base.documentStates)
|
|
379
451
|
.filter((e): e is [string, DocumentState] => !!e[1])
|
|
380
452
|
.map(([documentId, {unverifiedRevisions = {}, local, ...documentState}]) => {
|
|
381
453
|
const next: DocumentState = {
|
|
@@ -383,7 +455,7 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
|
|
|
383
455
|
local: documentId in working ? working[documentId] : local,
|
|
384
456
|
unverifiedRevisions:
|
|
385
457
|
prev.outgoing && prev.outgoing.transactionId in unverifiedRevisions
|
|
386
|
-
?
|
|
458
|
+
? omitProperty(unverifiedRevisions, prev.outgoing.transactionId)
|
|
387
459
|
: unverifiedRevisions,
|
|
388
460
|
}
|
|
389
461
|
return [documentId, next] as const
|
|
@@ -392,9 +464,29 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
|
|
|
392
464
|
}
|
|
393
465
|
}
|
|
394
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Extracts the patch operations addressed to the given document from a set of
|
|
469
|
+
* raw listener mutations, stripping the mutation-level `id` addressing so the
|
|
470
|
+
* patches are rooted at the document. A transaction can carry mutations for
|
|
471
|
+
* other documents, so anything not explicitly id-addressed to this document
|
|
472
|
+
* (including query-addressed patches) is dropped.
|
|
473
|
+
*/
|
|
474
|
+
function extractPatchOperations(
|
|
475
|
+
mutations: Mutation[] | undefined,
|
|
476
|
+
documentId: string,
|
|
477
|
+
): PatchOperations[] {
|
|
478
|
+
if (!mutations) return []
|
|
479
|
+
return mutations.flatMap((mutation) => {
|
|
480
|
+
if (!('patch' in mutation) || !mutation.patch) return []
|
|
481
|
+
const {id, ...operations} = mutation.patch as PatchOperations & {id?: string}
|
|
482
|
+
if (id !== documentId) return []
|
|
483
|
+
return [operations]
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
|
|
395
487
|
export function applyRemoteDocument(
|
|
396
488
|
prev: SyncTransactionState,
|
|
397
|
-
{document, documentId, previousRev, revision, timestamp, type}: RemoteDocument,
|
|
489
|
+
{document, documentId, previousRev, revision, timestamp, type, mutations}: RemoteDocument,
|
|
398
490
|
events: DocumentStoreState['events'],
|
|
399
491
|
): SyncTransactionState {
|
|
400
492
|
if (!prev.grants) return prev
|
|
@@ -411,7 +503,32 @@ export function applyRemoteDocument(
|
|
|
411
503
|
const revisionToVerify = revision ? prevUnverifiedRevisions?.[revision] : undefined
|
|
412
504
|
let unverifiedRevisions = prevUnverifiedRevisions ?? EMPTY_REVISIONS
|
|
413
505
|
if (revision && revisionToVerify) {
|
|
414
|
-
unverifiedRevisions =
|
|
506
|
+
unverifiedRevisions = omitProperty(prevUnverifiedRevisions, revision)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// surface the operational patches from this transaction before they are
|
|
510
|
+
// collapsed into the whole-document snapshot below. consumers that keep
|
|
511
|
+
// their own state (e.g. collaborative text editors) rely on these to apply
|
|
512
|
+
// remote changes without re-diffing document snapshots
|
|
513
|
+
if (type === 'mutation' && revision) {
|
|
514
|
+
const patches = extractPatchOperations(mutations, documentId)
|
|
515
|
+
if (patches.length) {
|
|
516
|
+
// `unverifiedRevisions` alone isn't a reliable origin marker: a sync
|
|
517
|
+
// event can prune an in-flight transaction's entry before its listener
|
|
518
|
+
// echo arrives. `recentOwnTransactionIds` survives that race
|
|
519
|
+
const isOwnTransaction =
|
|
520
|
+
Boolean(revisionToVerify) ||
|
|
521
|
+
(prevDocState.recentOwnTransactionIds?.includes(revision) ?? false)
|
|
522
|
+
events.next({
|
|
523
|
+
type: 'remote-patches',
|
|
524
|
+
documentId,
|
|
525
|
+
transactionId: revision,
|
|
526
|
+
previousRev,
|
|
527
|
+
timestamp,
|
|
528
|
+
patches,
|
|
529
|
+
origin: isOwnTransaction ? 'local' : 'remote',
|
|
530
|
+
})
|
|
531
|
+
}
|
|
415
532
|
}
|
|
416
533
|
|
|
417
534
|
// if this remote document is from a `'sync'` event (meaning that the whole
|
|
@@ -432,6 +549,34 @@ export function applyRemoteDocument(
|
|
|
432
549
|
// matches the previous revision we expected, we can "fast-forward" and skip
|
|
433
550
|
// rebasing local changes on top of this new base
|
|
434
551
|
if (revisionToVerify && revisionToVerify.previousRev === previousRev) {
|
|
552
|
+
// even on a clean fast-forward, the server's result can differ from our
|
|
553
|
+
// optimistic prediction: a publish derives the published content from the
|
|
554
|
+
// server's copy of the draft, which may carry concurrent edits our
|
|
555
|
+
// prediction never saw. once nothing else is pending locally for this
|
|
556
|
+
// document (no applied transaction and no in-flight transaction, other
|
|
557
|
+
// than the one this event just confirmed, modifies it), converge `local`
|
|
558
|
+
// to the server's document. while local work that modifies this document
|
|
559
|
+
// is pending, keep the optimistic `local` so those changes stay visible;
|
|
560
|
+
// that work's own echo (or rebase) recomputes `local` later.
|
|
561
|
+
//
|
|
562
|
+
// "modifies" matters: pending work on other documents must not block
|
|
563
|
+
// convergence here (nothing would ever revisit this document's `local`),
|
|
564
|
+
// and a pending draft edit carries the published document in its working
|
|
565
|
+
// set without changing it, so a presence check would block published-doc
|
|
566
|
+
// convergence whenever a draft edit is pending. this is the same
|
|
567
|
+
// modified-test `transitionAppliedTransactionsToOutgoing` uses
|
|
568
|
+
const modifiesDocument = (transaction: AppliedTransaction) =>
|
|
569
|
+
transaction.working[documentId]?._rev !== transaction.previousRevs[documentId]
|
|
570
|
+
const nothingElsePending =
|
|
571
|
+
!prev.applied.some(modifiesDocument) &&
|
|
572
|
+
(!prev.outgoing ||
|
|
573
|
+
prev.outgoing.transactionId === revision ||
|
|
574
|
+
!modifiesDocument(prev.outgoing))
|
|
575
|
+
const local =
|
|
576
|
+
nothingElsePending && !isDeepEqual(prevDocState.local, document)
|
|
577
|
+
? document
|
|
578
|
+
: prevDocState.local
|
|
579
|
+
|
|
435
580
|
return {
|
|
436
581
|
...prev,
|
|
437
582
|
documentStates: {
|
|
@@ -440,6 +585,7 @@ export function applyRemoteDocument(
|
|
|
440
585
|
...prevDocState,
|
|
441
586
|
remote: document,
|
|
442
587
|
remoteRev: revision,
|
|
588
|
+
local,
|
|
443
589
|
unverifiedRevisions,
|
|
444
590
|
},
|
|
445
591
|
},
|
|
@@ -466,7 +612,7 @@ export function applyRemoteDocument(
|
|
|
466
612
|
// transaction again through the listener and this same flow will run then
|
|
467
613
|
for (const curr of prev.applied) {
|
|
468
614
|
try {
|
|
469
|
-
const next = processActions({...curr, working, grants: prev.grants})
|
|
615
|
+
const next = processActions({...curr, working, grants: prev.grants, identity: prev.identity})
|
|
470
616
|
working = next.working
|
|
471
617
|
// next includes an updated `previous` set and `working` set and updates
|
|
472
618
|
// the `outgoingAction` and `outgoingMutations`. the `base` set from the
|
|
@@ -490,6 +636,12 @@ export function applyRemoteDocument(
|
|
|
490
636
|
}
|
|
491
637
|
}
|
|
492
638
|
|
|
639
|
+
// when the recompute lands on a value deep-equal to the current local
|
|
640
|
+
// (the common case for echoes of our own transactions), keep the previous
|
|
641
|
+
// reference so subscribers don't re-render for an identical value
|
|
642
|
+
const nextLocal = working[documentId]
|
|
643
|
+
const local = isDeepEqual(prevDocState.local, nextLocal) ? prevDocState.local : nextLocal
|
|
644
|
+
|
|
493
645
|
return {
|
|
494
646
|
...prev,
|
|
495
647
|
applied: nextApplied,
|
|
@@ -499,7 +651,7 @@ export function applyRemoteDocument(
|
|
|
499
651
|
...prevDocState,
|
|
500
652
|
remote: document,
|
|
501
653
|
remoteRev: revision,
|
|
502
|
-
local
|
|
654
|
+
local,
|
|
503
655
|
unverifiedRevisions,
|
|
504
656
|
},
|
|
505
657
|
},
|
|
@@ -538,7 +690,7 @@ export function removeSubscriptionIdFromDocument(
|
|
|
538
690
|
|
|
539
691
|
if (!prevDocState) return prev
|
|
540
692
|
if (!subscriptions.length) {
|
|
541
|
-
return {...prev, documentStates:
|
|
693
|
+
return {...prev, documentStates: omitProperty(prev.documentStates, documentId)}
|
|
542
694
|
}
|
|
543
695
|
return {
|
|
544
696
|
...prev,
|
|
@@ -551,22 +703,10 @@ export function removeSubscriptionIdFromDocument(
|
|
|
551
703
|
|
|
552
704
|
export function manageSubscriberIds(
|
|
553
705
|
{state}: StoreContext<SyncTransactionState>,
|
|
554
|
-
|
|
555
|
-
options?: {expandDraftPublished?: boolean},
|
|
706
|
+
handles: DocumentHandleLike[],
|
|
556
707
|
): () => 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
|
-
)
|
|
708
|
+
const documentIds = getDocumentIdsFromHandleLikes(handles)
|
|
709
|
+
|
|
570
710
|
const subscriptionId = insecureRandomId()
|
|
571
711
|
state.set('addSubscribers', (prev) =>
|
|
572
712
|
documentIds.reduce(
|
|
@@ -576,7 +716,7 @@ export function manageSubscriberIds(
|
|
|
576
716
|
)
|
|
577
717
|
|
|
578
718
|
return () => {
|
|
579
|
-
|
|
719
|
+
setCleanupTimeout(() => {
|
|
580
720
|
state.set('removeSubscribers', (prev) =>
|
|
581
721
|
documentIds.reduce(
|
|
582
722
|
(acc, id) => removeSubscriptionIdFromDocument(acc, id, subscriptionId),
|
|
@@ -587,13 +727,27 @@ export function manageSubscriberIds(
|
|
|
587
727
|
}
|
|
588
728
|
}
|
|
589
729
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
730
|
+
// document handles are passed in via the public facing API, but we also need to
|
|
731
|
+
// pull the correct document ids from action bodies, which have similar but not
|
|
732
|
+
// identical shapes to the document handles. release actions also flow through
|
|
733
|
+
// here, and resolve to the underlying release document id.
|
|
734
|
+
function getDocumentIdsFromHandleLikes(handles: (DocumentHandleLike | Action)[]): string[] {
|
|
735
|
+
return handles.flatMap((handle) => {
|
|
736
|
+
if ('type' in handle && isReleaseAction(handle)) {
|
|
737
|
+
return [getReleaseDocumentId(handle.releaseId)]
|
|
738
|
+
}
|
|
739
|
+
const idsForDocument = []
|
|
740
|
+
if (!handle.documentId) return []
|
|
741
|
+
if (handle.liveEdit) {
|
|
742
|
+
return [handle.documentId]
|
|
743
|
+
}
|
|
744
|
+
if (isReleasePerspective(handle.perspective)) {
|
|
745
|
+
idsForDocument.push(
|
|
746
|
+
getVersionId(DocumentId(handle.documentId), handle.perspective.releaseName),
|
|
747
|
+
)
|
|
748
|
+
}
|
|
749
|
+
idsForDocument.push(getPublishedId(DocumentId(handle.documentId)))
|
|
750
|
+
idsForDocument.push(getDraftId(DocumentId(handle.documentId)))
|
|
751
|
+
return idsForDocument
|
|
752
|
+
})
|
|
599
753
|
}
|
|
@@ -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
|
+
})
|