@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
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import {DocumentId, getDraftId, getPublishedId} from '@sanity/id-utils'
|
|
1
2
|
import {type SanityDocument} from '@sanity/types'
|
|
2
3
|
import {parse} from 'groq-js'
|
|
3
4
|
import {Subject} from 'rxjs'
|
|
4
5
|
import {describe, expect, it} from 'vitest'
|
|
5
6
|
|
|
6
|
-
import {getDraftId, getPublishedId} from '../utils/ids'
|
|
7
7
|
import {type DocumentEvent} from './events'
|
|
8
8
|
import {type RemoteDocument} from './listen'
|
|
9
9
|
import {type DocumentSet} from './processMutations'
|
|
@@ -56,7 +56,6 @@ describe('queueTransaction', () => {
|
|
|
56
56
|
documentId: 'doc1',
|
|
57
57
|
documentType: 'book',
|
|
58
58
|
patches: [{set: {foo: 'bar'}}],
|
|
59
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
60
59
|
},
|
|
61
60
|
],
|
|
62
61
|
}
|
|
@@ -67,8 +66,8 @@ describe('queueTransaction', () => {
|
|
|
67
66
|
expect(newState.queued[0]).toEqual(transaction)
|
|
68
67
|
|
|
69
68
|
// Check that both the published and draft documentStates got a subscription id added.
|
|
70
|
-
const draftId = getDraftId('doc1')
|
|
71
|
-
const pubId = getPublishedId('doc1')
|
|
69
|
+
const draftId = getDraftId(DocumentId('doc1'))
|
|
70
|
+
const pubId = getPublishedId(DocumentId('doc1'))
|
|
72
71
|
expect(newState.documentStates[draftId]).toBeDefined()
|
|
73
72
|
expect(newState.documentStates[pubId]).toBeDefined()
|
|
74
73
|
expect(newState.documentStates[draftId]?.subscriptions).toContain('txn1')
|
|
@@ -78,8 +77,8 @@ describe('queueTransaction', () => {
|
|
|
78
77
|
|
|
79
78
|
describe('removeQueuedTransaction', () => {
|
|
80
79
|
it('removes the transaction from queued and removes subscription ids from documents', () => {
|
|
81
|
-
const draftId = getDraftId('doc1')
|
|
82
|
-
const pubId = getPublishedId('doc1')
|
|
80
|
+
const draftId = getDraftId(DocumentId('doc1'))
|
|
81
|
+
const pubId = getPublishedId(DocumentId('doc1'))
|
|
83
82
|
|
|
84
83
|
const initialState: SyncTransactionState = {
|
|
85
84
|
queued: [
|
|
@@ -91,7 +90,6 @@ describe('removeQueuedTransaction', () => {
|
|
|
91
90
|
documentId: 'doc1',
|
|
92
91
|
documentType: 'book',
|
|
93
92
|
patches: [{set: {foo: 'bar'}}],
|
|
94
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
95
93
|
},
|
|
96
94
|
],
|
|
97
95
|
},
|
|
@@ -100,8 +98,8 @@ describe('removeQueuedTransaction', () => {
|
|
|
100
98
|
outgoing: undefined,
|
|
101
99
|
grants,
|
|
102
100
|
documentStates: {
|
|
103
|
-
[draftId]: {id: draftId, subscriptions: ['txn1']
|
|
104
|
-
[pubId]: {id: pubId, subscriptions: ['txn1']
|
|
101
|
+
[draftId]: {id: draftId, subscriptions: ['txn1']},
|
|
102
|
+
[pubId]: {id: pubId, subscriptions: ['txn1']},
|
|
105
103
|
} as SyncTransactionState['documentStates'],
|
|
106
104
|
}
|
|
107
105
|
|
|
@@ -140,19 +138,12 @@ describe('applyFirstQueuedTransaction', () => {
|
|
|
140
138
|
|
|
141
139
|
it('returns unchanged state if any required document is not yet loaded', () => {
|
|
142
140
|
// If a document's local value is undefined, the reducer should do nothing.
|
|
143
|
-
const draftId = getDraftId('doc1')
|
|
141
|
+
const draftId = getDraftId(DocumentId('doc1'))
|
|
144
142
|
const state: SyncTransactionState = {
|
|
145
143
|
queued: [
|
|
146
144
|
{
|
|
147
145
|
transactionId: 'txn2',
|
|
148
|
-
actions: [
|
|
149
|
-
{
|
|
150
|
-
type: 'document.discard',
|
|
151
|
-
documentId: 'doc1',
|
|
152
|
-
documentType: 'book',
|
|
153
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
154
|
-
},
|
|
155
|
-
],
|
|
146
|
+
actions: [{type: 'document.discard', documentId: 'doc1', documentType: 'book'}],
|
|
156
147
|
},
|
|
157
148
|
],
|
|
158
149
|
applied: [],
|
|
@@ -171,14 +162,7 @@ describe('applyFirstQueuedTransaction', () => {
|
|
|
171
162
|
queued: [
|
|
172
163
|
{
|
|
173
164
|
transactionId: 'txn-missing-grants',
|
|
174
|
-
actions: [
|
|
175
|
-
{
|
|
176
|
-
type: 'document.discard',
|
|
177
|
-
documentId: 'doc1',
|
|
178
|
-
documentType: 'book',
|
|
179
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
180
|
-
},
|
|
181
|
-
],
|
|
165
|
+
actions: [{type: 'document.discard', documentId: 'doc1', documentType: 'book'}],
|
|
182
166
|
},
|
|
183
167
|
],
|
|
184
168
|
applied: [],
|
|
@@ -192,8 +176,8 @@ describe('applyFirstQueuedTransaction', () => {
|
|
|
192
176
|
|
|
193
177
|
it('applies the first queued transaction (using a discard action)', () => {
|
|
194
178
|
// For a discard action, processActions deletes the draft document.
|
|
195
|
-
const draftId = getDraftId('doc1')
|
|
196
|
-
const pubId = getPublishedId('doc1')
|
|
179
|
+
const draftId = getDraftId(DocumentId('doc1'))
|
|
180
|
+
const pubId = getPublishedId(DocumentId('doc1'))
|
|
197
181
|
const initialDraft = {...exampleDoc, _id: draftId, foo: 'bar', _rev: 'rev1'}
|
|
198
182
|
const initialPub = {...exampleDoc, _id: pubId, foo: 'bar', _rev: 'rev1'}
|
|
199
183
|
|
|
@@ -201,14 +185,7 @@ describe('applyFirstQueuedTransaction', () => {
|
|
|
201
185
|
queued: [
|
|
202
186
|
{
|
|
203
187
|
transactionId: 'txn3',
|
|
204
|
-
actions: [
|
|
205
|
-
{
|
|
206
|
-
type: 'document.discard',
|
|
207
|
-
documentId: 'doc1',
|
|
208
|
-
documentType: 'book',
|
|
209
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
210
|
-
},
|
|
211
|
-
],
|
|
188
|
+
actions: [{type: 'document.discard', documentId: 'doc1', documentType: 'book'}],
|
|
212
189
|
},
|
|
213
190
|
],
|
|
214
191
|
applied: [],
|
|
@@ -247,31 +224,37 @@ describe('batchAppliedTransactions', () => {
|
|
|
247
224
|
documentId: 'doc1',
|
|
248
225
|
documentType: 'book',
|
|
249
226
|
patches: [{set: {foo: 'a'}}],
|
|
250
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
251
227
|
},
|
|
252
228
|
{
|
|
253
229
|
type: 'document.edit',
|
|
254
230
|
documentId: 'doc1',
|
|
255
231
|
documentType: 'book',
|
|
256
232
|
patches: [{set: {bar: 'b'}}],
|
|
257
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
258
233
|
},
|
|
259
234
|
],
|
|
260
235
|
disableBatching: false,
|
|
261
236
|
outgoingActions: [],
|
|
262
237
|
outgoingMutations: [],
|
|
263
|
-
base: {
|
|
238
|
+
base: {
|
|
239
|
+
[getDraftId(DocumentId('doc1'))]: {_id: getDraftId(DocumentId('doc1')), _rev: 'rev1'},
|
|
240
|
+
} as unknown as DocumentSet,
|
|
264
241
|
working: {
|
|
265
|
-
[getDraftId('doc1')]: {
|
|
242
|
+
[getDraftId(DocumentId('doc1'))]: {
|
|
266
243
|
...exampleDoc,
|
|
267
|
-
_id: getDraftId('doc1'),
|
|
244
|
+
_id: getDraftId(DocumentId('doc1')),
|
|
268
245
|
_rev: 'rev2',
|
|
269
246
|
foo: 'a',
|
|
270
247
|
bar: 'b',
|
|
271
248
|
},
|
|
272
249
|
},
|
|
273
|
-
previous: {
|
|
274
|
-
|
|
250
|
+
previous: {
|
|
251
|
+
[getDraftId(DocumentId('doc1'))]: {
|
|
252
|
+
...exampleDoc,
|
|
253
|
+
_id: getDraftId(DocumentId('doc1')),
|
|
254
|
+
_rev: 'rev1',
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
previousRevs: {[getDraftId(DocumentId('doc1'))]: 'rev1'},
|
|
275
258
|
timestamp: '2025-02-06T00:00:00.000Z',
|
|
276
259
|
}
|
|
277
260
|
const result = batchAppliedTransactions([appliedTx])
|
|
@@ -281,16 +264,16 @@ describe('batchAppliedTransactions', () => {
|
|
|
281
264
|
})
|
|
282
265
|
|
|
283
266
|
it('batches two edit transactions when possible', () => {
|
|
284
|
-
const
|
|
267
|
+
const documentId = DocumentId('doc1')
|
|
268
|
+
const draftId = getDraftId(documentId)
|
|
285
269
|
const appliedTx1: AppliedTransaction = {
|
|
286
270
|
transactionId: 'txn5',
|
|
287
271
|
actions: [
|
|
288
272
|
{
|
|
289
273
|
type: 'document.edit',
|
|
290
|
-
documentId
|
|
274
|
+
documentId,
|
|
291
275
|
documentType: 'book',
|
|
292
276
|
patches: [{set: {foo: 'a'}}],
|
|
293
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
294
277
|
},
|
|
295
278
|
],
|
|
296
279
|
disableBatching: false,
|
|
@@ -298,7 +281,7 @@ describe('batchAppliedTransactions', () => {
|
|
|
298
281
|
{
|
|
299
282
|
actionType: 'sanity.action.document.edit',
|
|
300
283
|
draftId,
|
|
301
|
-
publishedId: getPublishedId(
|
|
284
|
+
publishedId: getPublishedId(documentId),
|
|
302
285
|
patch: {set: {foo: 'a'}},
|
|
303
286
|
},
|
|
304
287
|
],
|
|
@@ -314,10 +297,9 @@ describe('batchAppliedTransactions', () => {
|
|
|
314
297
|
actions: [
|
|
315
298
|
{
|
|
316
299
|
type: 'document.edit',
|
|
317
|
-
documentId
|
|
300
|
+
documentId,
|
|
318
301
|
documentType: 'book',
|
|
319
302
|
patches: [{set: {bar: 'b'}}],
|
|
320
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
321
303
|
},
|
|
322
304
|
],
|
|
323
305
|
disableBatching: false,
|
|
@@ -325,7 +307,7 @@ describe('batchAppliedTransactions', () => {
|
|
|
325
307
|
{
|
|
326
308
|
actionType: 'sanity.action.document.edit',
|
|
327
309
|
draftId,
|
|
328
|
-
publishedId: getPublishedId(
|
|
310
|
+
publishedId: getPublishedId(documentId),
|
|
329
311
|
patch: {set: {bar: 'b'}},
|
|
330
312
|
},
|
|
331
313
|
],
|
|
@@ -349,7 +331,7 @@ describe('batchAppliedTransactions', () => {
|
|
|
349
331
|
})
|
|
350
332
|
|
|
351
333
|
it('returns a transaction with disableBatching true if a single edit action already has disableBatching set', () => {
|
|
352
|
-
const draftId = getDraftId('docA')
|
|
334
|
+
const draftId = getDraftId(DocumentId('docA'))
|
|
353
335
|
const appliedTx: AppliedTransaction = {
|
|
354
336
|
transactionId: 'txn-disable',
|
|
355
337
|
actions: [
|
|
@@ -358,7 +340,6 @@ describe('batchAppliedTransactions', () => {
|
|
|
358
340
|
documentId: 'docA',
|
|
359
341
|
documentType: 'book',
|
|
360
342
|
patches: [{set: {foo: 'a'}}],
|
|
361
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
362
343
|
},
|
|
363
344
|
],
|
|
364
345
|
disableBatching: true, // already set to true
|
|
@@ -378,6 +359,85 @@ describe('batchAppliedTransactions', () => {
|
|
|
378
359
|
// The actions array should be the same as the input's.
|
|
379
360
|
expect(result?.actions).toEqual(appliedTx.actions)
|
|
380
361
|
})
|
|
362
|
+
|
|
363
|
+
it('does not batch a liveEdit edit with a non-liveEdit edit', () => {
|
|
364
|
+
const nonLiveEditTx: AppliedTransaction = {
|
|
365
|
+
transactionId: 'txn-nonlive',
|
|
366
|
+
actions: [
|
|
367
|
+
{
|
|
368
|
+
type: 'document.edit',
|
|
369
|
+
documentId: 'doc1',
|
|
370
|
+
documentType: 'article',
|
|
371
|
+
patches: [{set: {foo: 'a'}}],
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
disableBatching: false,
|
|
375
|
+
outgoingActions: [
|
|
376
|
+
{
|
|
377
|
+
actionType: 'sanity.action.document.edit',
|
|
378
|
+
draftId: getDraftId(DocumentId('doc1')),
|
|
379
|
+
publishedId: getPublishedId(DocumentId('doc1')),
|
|
380
|
+
patch: {set: {foo: 'a'}},
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
outgoingMutations: [],
|
|
384
|
+
base: {
|
|
385
|
+
[getDraftId(DocumentId('doc1'))]: {
|
|
386
|
+
...exampleDoc,
|
|
387
|
+
_id: getDraftId(DocumentId('doc1')),
|
|
388
|
+
_rev: 'rev1',
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
working: {
|
|
392
|
+
[getDraftId(DocumentId('doc1'))]: {
|
|
393
|
+
...exampleDoc,
|
|
394
|
+
_id: getDraftId(DocumentId('doc1')),
|
|
395
|
+
_rev: 'rev2',
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
previous: {
|
|
399
|
+
[getDraftId(DocumentId('doc1'))]: {
|
|
400
|
+
...exampleDoc,
|
|
401
|
+
_id: getDraftId(DocumentId('doc1')),
|
|
402
|
+
_rev: 'rev1',
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
previousRevs: {[getDraftId(DocumentId('doc1'))]: 'rev1'},
|
|
406
|
+
timestamp: '2025-02-06T00:00:00.000Z',
|
|
407
|
+
}
|
|
408
|
+
const liveEditTx: AppliedTransaction = {
|
|
409
|
+
transactionId: 'txn-live',
|
|
410
|
+
actions: [
|
|
411
|
+
{
|
|
412
|
+
type: 'document.edit',
|
|
413
|
+
documentId: 'doc2',
|
|
414
|
+
documentType: 'liveArticle',
|
|
415
|
+
liveEdit: true,
|
|
416
|
+
patches: [{set: {bar: 'b'}}],
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
disableBatching: false,
|
|
420
|
+
outgoingActions: [],
|
|
421
|
+
outgoingMutations: [{patch: {id: 'doc2', set: {bar: 'b'}}}],
|
|
422
|
+
base: {doc2: {...exampleDoc, _id: 'doc2', _rev: 'rev1'}},
|
|
423
|
+
working: {doc2: {...exampleDoc, _id: 'doc2', _rev: 'rev2'}},
|
|
424
|
+
previous: {doc2: {...exampleDoc, _id: 'doc2', _rev: 'rev1'}},
|
|
425
|
+
previousRevs: {doc2: 'rev1'},
|
|
426
|
+
timestamp: '2025-02-06T00:01:00.000Z',
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// liveEdit first: should return only the liveEdit transaction
|
|
430
|
+
const resultLiveFirst = batchAppliedTransactions([liveEditTx, nonLiveEditTx])
|
|
431
|
+
expect(resultLiveFirst?.batchedTransactionIds).toEqual(['txn-live'])
|
|
432
|
+
expect(resultLiveFirst?.outgoingMutations).toEqual(liveEditTx.outgoingMutations)
|
|
433
|
+
expect(resultLiveFirst?.outgoingActions).toHaveLength(0)
|
|
434
|
+
|
|
435
|
+
// non-liveEdit first: should return only the non-liveEdit transaction
|
|
436
|
+
const resultNonLiveFirst = batchAppliedTransactions([nonLiveEditTx, liveEditTx])
|
|
437
|
+
expect(resultNonLiveFirst?.batchedTransactionIds).toEqual(['txn-nonlive'])
|
|
438
|
+
expect(resultNonLiveFirst?.outgoingActions).toEqual(nonLiveEditTx.outgoingActions)
|
|
439
|
+
expect(resultNonLiveFirst?.outgoingMutations).toHaveLength(0)
|
|
440
|
+
})
|
|
381
441
|
})
|
|
382
442
|
|
|
383
443
|
describe('transitionAppliedTransactionsToOutgoing', () => {
|
|
@@ -394,7 +454,7 @@ describe('transitionAppliedTransactionsToOutgoing', () => {
|
|
|
394
454
|
})
|
|
395
455
|
|
|
396
456
|
it('transitions applied transactions to an outgoing transaction', () => {
|
|
397
|
-
const draftId = getDraftId('doc1')
|
|
457
|
+
const draftId = getDraftId(DocumentId('doc1'))
|
|
398
458
|
const initialDoc = {...exampleDoc, _id: draftId, foo: 'old', _rev: 'rev1'}
|
|
399
459
|
const state: SyncTransactionState = {
|
|
400
460
|
queued: [],
|
|
@@ -407,7 +467,6 @@ describe('transitionAppliedTransactionsToOutgoing', () => {
|
|
|
407
467
|
documentId: 'doc1',
|
|
408
468
|
documentType: 'book',
|
|
409
469
|
patches: [{set: {foo: 'new'}}],
|
|
410
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
411
470
|
},
|
|
412
471
|
],
|
|
413
472
|
disableBatching: false,
|
|
@@ -438,6 +497,9 @@ describe('transitionAppliedTransactionsToOutgoing', () => {
|
|
|
438
497
|
expect(docState?.unverifiedRevisions).toBeDefined()
|
|
439
498
|
expect(docState?.unverifiedRevisions?.['txn7']).toBeDefined()
|
|
440
499
|
expect(docState?.unverifiedRevisions?.['txn7']?.previousRev).toEqual('rev1')
|
|
500
|
+
// the transaction ID is also tracked durably for remote-patches origin
|
|
501
|
+
// labeling, surviving unverified-revision pruning
|
|
502
|
+
expect(docState?.recentOwnTransactionIds).toEqual(['txn7'])
|
|
441
503
|
})
|
|
442
504
|
})
|
|
443
505
|
|
|
@@ -455,8 +517,8 @@ describe('cleanupOutgoingTransaction', () => {
|
|
|
455
517
|
})
|
|
456
518
|
|
|
457
519
|
it('removes subscription ids for all documents associated with the outgoing transaction and then clears outgoing', () => {
|
|
458
|
-
const draftId = getDraftId('doc1')
|
|
459
|
-
const pubId = getPublishedId('doc1')
|
|
520
|
+
const draftId = getDraftId(DocumentId('doc1'))
|
|
521
|
+
const pubId = getPublishedId(DocumentId('doc1'))
|
|
460
522
|
const state: SyncTransactionState = {
|
|
461
523
|
queued: [],
|
|
462
524
|
applied: [],
|
|
@@ -468,7 +530,6 @@ describe('cleanupOutgoingTransaction', () => {
|
|
|
468
530
|
documentId: 'doc1',
|
|
469
531
|
documentType: 'book',
|
|
470
532
|
patches: [{set: {foo: 'x'}}],
|
|
471
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
472
533
|
},
|
|
473
534
|
],
|
|
474
535
|
disableBatching: false,
|
|
@@ -497,8 +558,8 @@ describe('cleanupOutgoingTransaction', () => {
|
|
|
497
558
|
|
|
498
559
|
describe('revertOutgoingTransaction', () => {
|
|
499
560
|
it('reverts the outgoing transaction and updates documentStates by removing unverified revisions', () => {
|
|
500
|
-
const draftId = getDraftId('doc1')
|
|
501
|
-
const pubId = getPublishedId('doc1')
|
|
561
|
+
const draftId = getDraftId(DocumentId('doc1'))
|
|
562
|
+
const pubId = getPublishedId(DocumentId('doc1'))
|
|
502
563
|
// In this test we simulate a state with one applied transaction and an outgoing transaction.
|
|
503
564
|
const state: SyncTransactionState = {
|
|
504
565
|
queued: [],
|
|
@@ -511,7 +572,6 @@ describe('revertOutgoingTransaction', () => {
|
|
|
511
572
|
documentId: 'doc1',
|
|
512
573
|
documentType: 'book',
|
|
513
574
|
patches: [{set: {foo: 'reverted'}}],
|
|
514
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
515
575
|
},
|
|
516
576
|
],
|
|
517
577
|
disableBatching: false,
|
|
@@ -532,7 +592,6 @@ describe('revertOutgoingTransaction', () => {
|
|
|
532
592
|
documentId: 'doc1',
|
|
533
593
|
documentType: 'book',
|
|
534
594
|
patches: [{set: {foo: 'changed'}}],
|
|
535
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
536
595
|
},
|
|
537
596
|
],
|
|
538
597
|
disableBatching: false,
|
|
@@ -597,7 +656,7 @@ describe('applyRemoteDocument', () => {
|
|
|
597
656
|
})
|
|
598
657
|
|
|
599
658
|
it('verifies an unverified revision when the revision matches and previousRev is as expected', () => {
|
|
600
|
-
const docId = getDraftId('doc1')
|
|
659
|
+
const docId = getDraftId(DocumentId('doc1'))
|
|
601
660
|
const initialState: SyncTransactionState = {
|
|
602
661
|
queued: [],
|
|
603
662
|
applied: [],
|
|
@@ -637,9 +696,132 @@ describe('applyRemoteDocument', () => {
|
|
|
637
696
|
expect(newDocState?.unverifiedRevisions?.['txn10']).toBeUndefined()
|
|
638
697
|
})
|
|
639
698
|
|
|
699
|
+
describe('fast-forward convergence of local', () => {
|
|
700
|
+
const docId = getDraftId(DocumentId('doc1'))
|
|
701
|
+
const otherDocId = getDraftId(DocumentId('doc2'))
|
|
702
|
+
|
|
703
|
+
const makeApplied = (overrides: Partial<AppliedTransaction>): AppliedTransaction => ({
|
|
704
|
+
transactionId: 'txnPending',
|
|
705
|
+
actions: [],
|
|
706
|
+
working: {},
|
|
707
|
+
previous: {},
|
|
708
|
+
base: {},
|
|
709
|
+
previousRevs: {},
|
|
710
|
+
timestamp: '2025-02-06T00:06:30.000Z',
|
|
711
|
+
outgoingActions: [],
|
|
712
|
+
outgoingMutations: [],
|
|
713
|
+
...overrides,
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
const makeState = (applied: AppliedTransaction[]): SyncTransactionState => ({
|
|
717
|
+
queued: [],
|
|
718
|
+
applied,
|
|
719
|
+
outgoing: undefined,
|
|
720
|
+
grants,
|
|
721
|
+
documentStates: {
|
|
722
|
+
[docId]: {
|
|
723
|
+
id: docId,
|
|
724
|
+
subscriptions: ['sub1'],
|
|
725
|
+
local: {...exampleDoc, _id: docId, foo: 'optimistic'},
|
|
726
|
+
remote: {...exampleDoc, _id: docId, foo: 'old'},
|
|
727
|
+
unverifiedRevisions: {
|
|
728
|
+
txn10: {
|
|
729
|
+
transactionId: 'txn10',
|
|
730
|
+
documentId: docId,
|
|
731
|
+
previousRev: 'revOld',
|
|
732
|
+
timestamp: '2025-02-06T00:06:00.000Z',
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
const remote: RemoteDocument = {
|
|
740
|
+
type: 'sync',
|
|
741
|
+
documentId: docId,
|
|
742
|
+
document: {...exampleDoc, _id: docId, foo: 'server-truth'},
|
|
743
|
+
revision: 'txn10',
|
|
744
|
+
previousRev: 'revOld',
|
|
745
|
+
timestamp: '2025-02-06T00:07:00.000Z',
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
it('converges local to the server document when nothing else is pending', () => {
|
|
749
|
+
const events = new Subject<DocumentEvent>()
|
|
750
|
+
const newState = applyRemoteDocument(makeState([]), remote, events)
|
|
751
|
+
expect(newState.documentStates[docId]?.local).toEqual(remote.document)
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
it('converges local when pending work modifies only other documents', () => {
|
|
755
|
+
// a pending applied transaction on doc2 must not block convergence of
|
|
756
|
+
// doc1: nothing would ever revisit doc1's local otherwise
|
|
757
|
+
const otherDoc = {...exampleDoc, _id: otherDocId}
|
|
758
|
+
const pendingOnOtherDoc = makeApplied({
|
|
759
|
+
working: {[otherDocId]: {...otherDoc, _rev: 'txnPending'}},
|
|
760
|
+
previous: {[otherDocId]: otherDoc},
|
|
761
|
+
base: {[otherDocId]: otherDoc},
|
|
762
|
+
previousRevs: {[otherDocId]: 'txn0'},
|
|
763
|
+
})
|
|
764
|
+
const events = new Subject<DocumentEvent>()
|
|
765
|
+
const newState = applyRemoteDocument(makeState([pendingOnOtherDoc]), remote, events)
|
|
766
|
+
expect(newState.documentStates[docId]?.local).toEqual(remote.document)
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
it('converges local when pending work carries this document unmodified', () => {
|
|
770
|
+
// a draft edit carries the published document in its working set
|
|
771
|
+
// without changing it; presence alone must not block convergence
|
|
772
|
+
const thisDoc = {...exampleDoc, _id: docId}
|
|
773
|
+
const pendingCarryingUnmodified = makeApplied({
|
|
774
|
+
working: {
|
|
775
|
+
[docId]: thisDoc,
|
|
776
|
+
[otherDocId]: {...exampleDoc, _id: otherDocId, _rev: 'txnPending'},
|
|
777
|
+
},
|
|
778
|
+
previous: {[docId]: thisDoc, [otherDocId]: {...exampleDoc, _id: otherDocId}},
|
|
779
|
+
base: {[docId]: thisDoc, [otherDocId]: {...exampleDoc, _id: otherDocId}},
|
|
780
|
+
previousRevs: {[docId]: thisDoc._rev, [otherDocId]: 'txn0'},
|
|
781
|
+
})
|
|
782
|
+
const events = new Subject<DocumentEvent>()
|
|
783
|
+
const newState = applyRemoteDocument(makeState([pendingCarryingUnmodified]), remote, events)
|
|
784
|
+
expect(newState.documentStates[docId]?.local).toEqual(remote.document)
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
it('keeps the optimistic local when pending work modifies this document', () => {
|
|
788
|
+
const optimistic = {...exampleDoc, _id: docId, _rev: 'txnPending', foo: 'optimistic'}
|
|
789
|
+
const pendingOnThisDoc = makeApplied({
|
|
790
|
+
working: {[docId]: optimistic},
|
|
791
|
+
previous: {[docId]: {...exampleDoc, _id: docId}},
|
|
792
|
+
base: {[docId]: {...exampleDoc, _id: docId}},
|
|
793
|
+
previousRevs: {[docId]: 'txn0'},
|
|
794
|
+
})
|
|
795
|
+
const state = makeState([pendingOnThisDoc])
|
|
796
|
+
const events = new Subject<DocumentEvent>()
|
|
797
|
+
const newState = applyRemoteDocument(state, remote, events)
|
|
798
|
+
expect(newState.documentStates[docId]?.local).toBe(state.documentStates[docId]?.local)
|
|
799
|
+
expect(newState.documentStates[docId]?.remote).toEqual(remote.document)
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
it('keeps the optimistic local when a different in-flight transaction modifies this document', () => {
|
|
803
|
+
const optimistic = {...exampleDoc, _id: docId, _rev: 'txnOutgoing', foo: 'optimistic'}
|
|
804
|
+
const outgoing = {
|
|
805
|
+
...makeApplied({
|
|
806
|
+
transactionId: 'txnOutgoing',
|
|
807
|
+
working: {[docId]: optimistic},
|
|
808
|
+
previous: {[docId]: {...exampleDoc, _id: docId}},
|
|
809
|
+
base: {[docId]: {...exampleDoc, _id: docId}},
|
|
810
|
+
previousRevs: {[docId]: 'txn0'},
|
|
811
|
+
}),
|
|
812
|
+
disableBatching: false,
|
|
813
|
+
batchedTransactionIds: ['txnOutgoing'],
|
|
814
|
+
}
|
|
815
|
+
const state = {...makeState([]), outgoing}
|
|
816
|
+
const events = new Subject<DocumentEvent>()
|
|
817
|
+
const newState = applyRemoteDocument(state, remote, events)
|
|
818
|
+
expect(newState.documentStates[docId]?.local).toBe(state.documentStates[docId]?.local)
|
|
819
|
+
})
|
|
820
|
+
})
|
|
821
|
+
|
|
640
822
|
it('rebases local changes when no matching unverified revision is found', () => {
|
|
641
823
|
// In this branch we simply let processActions rebase so that the local becomes the remote.
|
|
642
|
-
const docId = getDraftId('doc1')
|
|
824
|
+
const docId = getDraftId(DocumentId('doc1'))
|
|
643
825
|
const initialState: SyncTransactionState = {
|
|
644
826
|
queued: [],
|
|
645
827
|
applied: [],
|
|
@@ -674,7 +856,7 @@ describe('applyRemoteDocument', () => {
|
|
|
674
856
|
|
|
675
857
|
// Test that a sync event removes outdated unverified revisions.
|
|
676
858
|
it('removes outdated unverified revisions when a sync event is received', () => {
|
|
677
|
-
const docId = getDraftId('doc1')
|
|
859
|
+
const docId = getDraftId(DocumentId('doc1'))
|
|
678
860
|
// An unverified revision created at an earlier time.
|
|
679
861
|
const outdatedTimestamp = new Date('2025-02-06T00:09:00.000Z').toISOString()
|
|
680
862
|
// The incoming sync event timestamp is later.
|
|
@@ -738,6 +920,399 @@ describe('applyRemoteDocument', () => {
|
|
|
738
920
|
expect(newDocState?.remote).toEqual(remote.document)
|
|
739
921
|
expect(newDocState?.remoteRev).toBe('currentRev')
|
|
740
922
|
})
|
|
923
|
+
|
|
924
|
+
describe('remote-patches events', () => {
|
|
925
|
+
const docId = getDraftId(DocumentId('doc1'))
|
|
926
|
+
|
|
927
|
+
function createInitialState(
|
|
928
|
+
unverifiedRevisions: NonNullable<
|
|
929
|
+
SyncTransactionState['documentStates'][string]
|
|
930
|
+
>['unverifiedRevisions'] = {},
|
|
931
|
+
recentOwnTransactionIds?: string[],
|
|
932
|
+
): SyncTransactionState {
|
|
933
|
+
return {
|
|
934
|
+
queued: [],
|
|
935
|
+
applied: [],
|
|
936
|
+
outgoing: undefined,
|
|
937
|
+
grants,
|
|
938
|
+
documentStates: {
|
|
939
|
+
[docId]: {
|
|
940
|
+
id: docId,
|
|
941
|
+
subscriptions: ['sub1'],
|
|
942
|
+
local: {...exampleDoc, _id: docId, foo: 'old'},
|
|
943
|
+
remote: {...exampleDoc, _id: docId, foo: 'old'},
|
|
944
|
+
unverifiedRevisions,
|
|
945
|
+
...(recentOwnTransactionIds && {recentOwnTransactionIds}),
|
|
946
|
+
},
|
|
947
|
+
},
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
it('emits a remote-patches event with origin "remote" for foreign transactions', () => {
|
|
952
|
+
const events = new Subject<DocumentEvent>()
|
|
953
|
+
const received: DocumentEvent[] = []
|
|
954
|
+
events.subscribe((e) => received.push(e))
|
|
955
|
+
|
|
956
|
+
const remote: RemoteDocument = {
|
|
957
|
+
type: 'mutation',
|
|
958
|
+
documentId: docId,
|
|
959
|
+
document: {...exampleDoc, _id: docId, foo: 'new'},
|
|
960
|
+
revision: 'txnForeign',
|
|
961
|
+
previousRev: 'txn0',
|
|
962
|
+
timestamp: '2025-02-06T00:12:00.000Z',
|
|
963
|
+
mutations: [
|
|
964
|
+
{
|
|
965
|
+
patch: {
|
|
966
|
+
id: docId,
|
|
967
|
+
unset: ['blocks[_key=="a"].children[_key=="b"].marks[0]'],
|
|
968
|
+
},
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
patch: {
|
|
972
|
+
id: docId,
|
|
973
|
+
insert: {after: 'blocks[_key=="a"].markDefs[-1]', items: [{_key: 'link1'}]},
|
|
974
|
+
},
|
|
975
|
+
},
|
|
976
|
+
],
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
applyRemoteDocument(createInitialState(), remote, events)
|
|
980
|
+
|
|
981
|
+
expect(received).toEqual([
|
|
982
|
+
{
|
|
983
|
+
type: 'remote-patches',
|
|
984
|
+
documentId: docId,
|
|
985
|
+
transactionId: 'txnForeign',
|
|
986
|
+
previousRev: 'txn0',
|
|
987
|
+
timestamp: '2025-02-06T00:12:00.000Z',
|
|
988
|
+
origin: 'remote',
|
|
989
|
+
patches: [
|
|
990
|
+
{unset: ['blocks[_key=="a"].children[_key=="b"].marks[0]']},
|
|
991
|
+
{insert: {after: 'blocks[_key=="a"].markDefs[-1]', items: [{_key: 'link1'}]}},
|
|
992
|
+
],
|
|
993
|
+
},
|
|
994
|
+
])
|
|
995
|
+
})
|
|
996
|
+
|
|
997
|
+
it('emits origin "local" when the transaction is one of our own unverified revisions', () => {
|
|
998
|
+
const events = new Subject<DocumentEvent>()
|
|
999
|
+
const received: DocumentEvent[] = []
|
|
1000
|
+
events.subscribe((e) => received.push(e))
|
|
1001
|
+
|
|
1002
|
+
const remote: RemoteDocument = {
|
|
1003
|
+
type: 'mutation',
|
|
1004
|
+
documentId: docId,
|
|
1005
|
+
document: {...exampleDoc, _id: docId, foo: 'new'},
|
|
1006
|
+
revision: 'txnOwn',
|
|
1007
|
+
previousRev: 'txn0',
|
|
1008
|
+
timestamp: '2025-02-06T00:13:00.000Z',
|
|
1009
|
+
mutations: [{patch: {id: docId, set: {foo: 'new'}}}],
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
applyRemoteDocument(
|
|
1013
|
+
createInitialState({
|
|
1014
|
+
txnOwn: {
|
|
1015
|
+
transactionId: 'txnOwn',
|
|
1016
|
+
documentId: docId,
|
|
1017
|
+
previousRev: 'txn0',
|
|
1018
|
+
timestamp: '2025-02-06T00:13:00.000Z',
|
|
1019
|
+
},
|
|
1020
|
+
}),
|
|
1021
|
+
remote,
|
|
1022
|
+
events,
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
expect(received).toEqual([
|
|
1026
|
+
{
|
|
1027
|
+
type: 'remote-patches',
|
|
1028
|
+
documentId: docId,
|
|
1029
|
+
transactionId: 'txnOwn',
|
|
1030
|
+
previousRev: 'txn0',
|
|
1031
|
+
timestamp: '2025-02-06T00:13:00.000Z',
|
|
1032
|
+
origin: 'local',
|
|
1033
|
+
patches: [{set: {foo: 'new'}}],
|
|
1034
|
+
},
|
|
1035
|
+
])
|
|
1036
|
+
})
|
|
1037
|
+
|
|
1038
|
+
it('does not emit for sync events', () => {
|
|
1039
|
+
const events = new Subject<DocumentEvent>()
|
|
1040
|
+
const received: DocumentEvent[] = []
|
|
1041
|
+
events.subscribe((e) => received.push(e))
|
|
1042
|
+
|
|
1043
|
+
const remote: RemoteDocument = {
|
|
1044
|
+
type: 'sync',
|
|
1045
|
+
documentId: docId,
|
|
1046
|
+
document: {...exampleDoc, _id: docId, foo: 'new'},
|
|
1047
|
+
revision: 'revSync',
|
|
1048
|
+
timestamp: '2025-02-06T00:14:00.000Z',
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
applyRemoteDocument(createInitialState(), remote, events)
|
|
1052
|
+
expect(received).toEqual([])
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1055
|
+
it('does not emit when the mutations contain no patches', () => {
|
|
1056
|
+
const events = new Subject<DocumentEvent>()
|
|
1057
|
+
const received: DocumentEvent[] = []
|
|
1058
|
+
events.subscribe((e) => received.push(e))
|
|
1059
|
+
|
|
1060
|
+
const remote: RemoteDocument = {
|
|
1061
|
+
type: 'mutation',
|
|
1062
|
+
documentId: docId,
|
|
1063
|
+
document: {...exampleDoc, _id: docId, foo: 'new'},
|
|
1064
|
+
revision: 'txnCreate',
|
|
1065
|
+
previousRev: 'txn0',
|
|
1066
|
+
timestamp: '2025-02-06T00:15:00.000Z',
|
|
1067
|
+
mutations: [{createOrReplace: {...exampleDoc, _id: docId, foo: 'new'}}],
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
applyRemoteDocument(createInitialState(), remote, events)
|
|
1071
|
+
expect(received).toEqual([])
|
|
1072
|
+
})
|
|
1073
|
+
|
|
1074
|
+
it('excludes patches addressed to other documents in the same transaction', () => {
|
|
1075
|
+
const events = new Subject<DocumentEvent>()
|
|
1076
|
+
const received: DocumentEvent[] = []
|
|
1077
|
+
events.subscribe((e) => received.push(e))
|
|
1078
|
+
|
|
1079
|
+
const remote: RemoteDocument = {
|
|
1080
|
+
type: 'mutation',
|
|
1081
|
+
documentId: docId,
|
|
1082
|
+
document: {...exampleDoc, _id: docId, foo: 'new'},
|
|
1083
|
+
revision: 'txnMulti',
|
|
1084
|
+
previousRev: 'txn0',
|
|
1085
|
+
timestamp: '2025-02-06T00:20:00.000Z',
|
|
1086
|
+
mutations: [
|
|
1087
|
+
{patch: {id: docId, set: {foo: 'new'}}},
|
|
1088
|
+
{patch: {id: 'some-other-doc', set: {foo: 'other'}}},
|
|
1089
|
+
{patch: {query: '*[_type == "author"]', set: {foo: 'queried'}}},
|
|
1090
|
+
],
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
applyRemoteDocument(createInitialState(), remote, events)
|
|
1094
|
+
|
|
1095
|
+
expect(received).toHaveLength(1)
|
|
1096
|
+
expect((received[0] as {patches: unknown}).patches).toEqual([{set: {foo: 'new'}}])
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1099
|
+
it('labels origin "local" via recentOwnTransactionIds when the unverified revision was pruned', () => {
|
|
1100
|
+
const events = new Subject<DocumentEvent>()
|
|
1101
|
+
const received: DocumentEvent[] = []
|
|
1102
|
+
events.subscribe((e) => received.push(e))
|
|
1103
|
+
|
|
1104
|
+
const remote: RemoteDocument = {
|
|
1105
|
+
type: 'mutation',
|
|
1106
|
+
documentId: docId,
|
|
1107
|
+
document: {...exampleDoc, _id: docId, foo: 'new'},
|
|
1108
|
+
revision: 'txnPruned',
|
|
1109
|
+
previousRev: 'txn0',
|
|
1110
|
+
timestamp: '2025-02-06T00:21:00.000Z',
|
|
1111
|
+
mutations: [{patch: {id: docId, set: {foo: 'new'}}}],
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// a sync event racing the listener echo pruned the unverified revision,
|
|
1115
|
+
// but the transaction ID is still tracked as one of our own
|
|
1116
|
+
applyRemoteDocument(createInitialState({}, ['txnPruned']), remote, events)
|
|
1117
|
+
|
|
1118
|
+
expect(received).toHaveLength(1)
|
|
1119
|
+
expect(received[0]).toMatchObject({
|
|
1120
|
+
type: 'remote-patches',
|
|
1121
|
+
transactionId: 'txnPruned',
|
|
1122
|
+
origin: 'local',
|
|
1123
|
+
})
|
|
1124
|
+
})
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
describe('rebase with preserved operations', () => {
|
|
1128
|
+
it('skips a preserved-operations transaction that fails to re-apply and emits rebase-error', () => {
|
|
1129
|
+
const docId = getDraftId(DocumentId('doc1'))
|
|
1130
|
+
const originalDoc: SanityDocument = {
|
|
1131
|
+
...exampleDoc,
|
|
1132
|
+
_id: docId,
|
|
1133
|
+
_rev: 'rev1',
|
|
1134
|
+
title: 'The quick brown fox',
|
|
1135
|
+
}
|
|
1136
|
+
const locallyEditedDoc: SanityDocument = {
|
|
1137
|
+
...originalDoc,
|
|
1138
|
+
title: 'The quick brown cat',
|
|
1139
|
+
_rev: 'txnLocal',
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const appliedTransaction: AppliedTransaction = {
|
|
1143
|
+
transactionId: 'txnLocal',
|
|
1144
|
+
actions: [
|
|
1145
|
+
{
|
|
1146
|
+
type: 'document.edit',
|
|
1147
|
+
documentId: 'doc1',
|
|
1148
|
+
documentType: 'author',
|
|
1149
|
+
patches: [{diffMatchPatch: {title: '@@ -13,7 +13,7 @@\n own \n-fox\n+cat\n'}}],
|
|
1150
|
+
preserveOperations: true,
|
|
1151
|
+
},
|
|
1152
|
+
],
|
|
1153
|
+
previous: {[docId]: originalDoc},
|
|
1154
|
+
base: {[docId]: originalDoc},
|
|
1155
|
+
working: {[docId]: locallyEditedDoc},
|
|
1156
|
+
previousRevs: {[docId]: 'rev1'},
|
|
1157
|
+
timestamp: '2025-02-06T00:16:00.000Z',
|
|
1158
|
+
outgoingActions: [],
|
|
1159
|
+
outgoingMutations: [],
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const initialState: SyncTransactionState = {
|
|
1163
|
+
queued: [],
|
|
1164
|
+
applied: [appliedTransaction],
|
|
1165
|
+
outgoing: undefined,
|
|
1166
|
+
grants,
|
|
1167
|
+
documentStates: {
|
|
1168
|
+
[docId]: {
|
|
1169
|
+
id: docId,
|
|
1170
|
+
subscriptions: ['sub1'],
|
|
1171
|
+
local: locallyEditedDoc,
|
|
1172
|
+
remote: originalDoc,
|
|
1173
|
+
unverifiedRevisions: {},
|
|
1174
|
+
},
|
|
1175
|
+
},
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// a foreign transaction replaced the title with a non-string, so our
|
|
1179
|
+
// diffMatchPatch can no longer re-apply during the rebase
|
|
1180
|
+
const remoteDoc: SanityDocument = {
|
|
1181
|
+
...originalDoc,
|
|
1182
|
+
title: 42 as unknown as string,
|
|
1183
|
+
_rev: 'txnForeign',
|
|
1184
|
+
}
|
|
1185
|
+
const remote: RemoteDocument = {
|
|
1186
|
+
type: 'mutation',
|
|
1187
|
+
documentId: docId,
|
|
1188
|
+
document: remoteDoc,
|
|
1189
|
+
revision: 'txnForeign',
|
|
1190
|
+
previousRev: 'rev1',
|
|
1191
|
+
timestamp: '2025-02-06T00:17:00.000Z',
|
|
1192
|
+
mutations: [{patch: {id: docId, set: {title: 42}}}],
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const events = new Subject<DocumentEvent>()
|
|
1196
|
+
const received: DocumentEvent[] = []
|
|
1197
|
+
events.subscribe((e) => received.push(e))
|
|
1198
|
+
|
|
1199
|
+
const newState = applyRemoteDocument(initialState, remote, events)
|
|
1200
|
+
|
|
1201
|
+
const rebaseErrors = received.filter((e) => e.type === 'rebase-error')
|
|
1202
|
+
expect(rebaseErrors).toHaveLength(1)
|
|
1203
|
+
expect(rebaseErrors[0]).toMatchObject({
|
|
1204
|
+
type: 'rebase-error',
|
|
1205
|
+
transactionId: 'txnLocal',
|
|
1206
|
+
documentId: 'doc1',
|
|
1207
|
+
message: expect.stringMatching(/Failed to apply patches to the working document/),
|
|
1208
|
+
})
|
|
1209
|
+
|
|
1210
|
+
// the failing transaction is dropped and local converges to remote
|
|
1211
|
+
expect(newState.applied).toEqual([])
|
|
1212
|
+
expect(newState.documentStates[docId]?.local).toEqual(remoteDoc)
|
|
1213
|
+
expect(newState.documentStates[docId]?.remote).toEqual(remoteDoc)
|
|
1214
|
+
})
|
|
1215
|
+
|
|
1216
|
+
it('re-applies preserved keyed patches onto a diverged remote document', () => {
|
|
1217
|
+
const docId = getDraftId(DocumentId('doc1'))
|
|
1218
|
+
const originalDoc: SanityDocument = {
|
|
1219
|
+
...exampleDoc,
|
|
1220
|
+
_id: docId,
|
|
1221
|
+
_rev: 'rev1',
|
|
1222
|
+
items: [
|
|
1223
|
+
{_key: 'a', value: 'first'},
|
|
1224
|
+
{_key: 'b', value: 'second'},
|
|
1225
|
+
],
|
|
1226
|
+
}
|
|
1227
|
+
const locallyEditedDoc: SanityDocument = {
|
|
1228
|
+
...originalDoc,
|
|
1229
|
+
items: [
|
|
1230
|
+
{_key: 'a', value: 'first'},
|
|
1231
|
+
{_key: 'c', value: 'in between'},
|
|
1232
|
+
{_key: 'b', value: 'second'},
|
|
1233
|
+
],
|
|
1234
|
+
_rev: 'txnLocal',
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const appliedTransaction: AppliedTransaction = {
|
|
1238
|
+
transactionId: 'txnLocal',
|
|
1239
|
+
actions: [
|
|
1240
|
+
{
|
|
1241
|
+
type: 'document.edit',
|
|
1242
|
+
documentId: 'doc1',
|
|
1243
|
+
documentType: 'author',
|
|
1244
|
+
patches: [
|
|
1245
|
+
{insert: {after: 'items[_key=="a"]', items: [{_key: 'c', value: 'in between'}]}},
|
|
1246
|
+
],
|
|
1247
|
+
preserveOperations: true,
|
|
1248
|
+
},
|
|
1249
|
+
],
|
|
1250
|
+
previous: {[docId]: originalDoc},
|
|
1251
|
+
base: {[docId]: originalDoc},
|
|
1252
|
+
working: {[docId]: locallyEditedDoc},
|
|
1253
|
+
previousRevs: {[docId]: 'rev1'},
|
|
1254
|
+
timestamp: '2025-02-06T00:18:00.000Z',
|
|
1255
|
+
outgoingActions: [],
|
|
1256
|
+
outgoingMutations: [],
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const initialState: SyncTransactionState = {
|
|
1260
|
+
queued: [],
|
|
1261
|
+
applied: [appliedTransaction],
|
|
1262
|
+
outgoing: undefined,
|
|
1263
|
+
grants,
|
|
1264
|
+
documentStates: {
|
|
1265
|
+
[docId]: {
|
|
1266
|
+
id: docId,
|
|
1267
|
+
subscriptions: ['sub1'],
|
|
1268
|
+
local: locallyEditedDoc,
|
|
1269
|
+
remote: originalDoc,
|
|
1270
|
+
unverifiedRevisions: {},
|
|
1271
|
+
},
|
|
1272
|
+
},
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// a foreign transaction appended a new item concurrently
|
|
1276
|
+
const remoteDoc: SanityDocument = {
|
|
1277
|
+
...originalDoc,
|
|
1278
|
+
items: [
|
|
1279
|
+
{_key: 'a', value: 'first'},
|
|
1280
|
+
{_key: 'b', value: 'second'},
|
|
1281
|
+
{_key: 'd', value: 'appended remotely'},
|
|
1282
|
+
],
|
|
1283
|
+
_rev: 'txnForeign',
|
|
1284
|
+
}
|
|
1285
|
+
const remote: RemoteDocument = {
|
|
1286
|
+
type: 'mutation',
|
|
1287
|
+
documentId: docId,
|
|
1288
|
+
document: remoteDoc,
|
|
1289
|
+
revision: 'txnForeign',
|
|
1290
|
+
previousRev: 'rev1',
|
|
1291
|
+
timestamp: '2025-02-06T00:19:00.000Z',
|
|
1292
|
+
mutations: [
|
|
1293
|
+
{
|
|
1294
|
+
patch: {
|
|
1295
|
+
id: docId,
|
|
1296
|
+
insert: {after: 'items[-1]', items: [{_key: 'd', value: 'appended remotely'}]},
|
|
1297
|
+
},
|
|
1298
|
+
},
|
|
1299
|
+
],
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const events = new Subject<DocumentEvent>()
|
|
1303
|
+
const newState = applyRemoteDocument(initialState, remote, events)
|
|
1304
|
+
|
|
1305
|
+
// both edits survive: our keyed insert re-anchors after item "a" while
|
|
1306
|
+
// the remote append is preserved
|
|
1307
|
+
expect(newState.documentStates[docId]?.local?.['items']).toEqual([
|
|
1308
|
+
{_key: 'a', value: 'first'},
|
|
1309
|
+
{_key: 'c', value: 'in between'},
|
|
1310
|
+
{_key: 'b', value: 'second'},
|
|
1311
|
+
{_key: 'd', value: 'appended remotely'},
|
|
1312
|
+
])
|
|
1313
|
+
expect(newState.applied).toHaveLength(1)
|
|
1314
|
+
})
|
|
1315
|
+
})
|
|
741
1316
|
})
|
|
742
1317
|
|
|
743
1318
|
describe('addSubscriptionIdToDocument', () => {
|