@sanity/sdk 2.15.0 → 2.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts +5 -4
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +152 -148
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/version.js +1 -1
- package/dist/index.d.ts +69 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -32
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/src/_exports/index.ts +2 -0
- package/src/auth/authStore.test.ts +1 -1
- package/src/auth/handleAuthCallback.test.ts +1 -1
- package/src/auth/refreshStampedToken.test.ts +199 -477
- package/src/auth/refreshStampedToken.ts +67 -195
- package/src/client/liveEvents.test.ts +151 -0
- package/src/client/liveEvents.ts +107 -0
- package/src/document/actions.ts +36 -0
- package/src/document/documentStore.test.ts +132 -1
- package/src/document/documentStore.ts +8 -0
- package/src/document/events.ts +32 -0
- package/src/document/listen.ts +8 -0
- package/src/document/processActions/edit.ts +19 -14
- package/src/document/processActions/shared.ts +55 -8
- package/src/document/processActions.test.ts +145 -0
- package/src/document/reducers.test.ts +396 -0
- package/src/document/reducers.ts +62 -1
- package/src/document/sharedListener.test.ts +1 -0
- package/src/document/sharedListener.ts +2 -0
- package/src/query/queryStore.test.ts +232 -38
- package/src/query/queryStore.ts +47 -47
- package/src/query/reducers.test.ts +0 -42
- package/src/query/reducers.ts +0 -16
- package/src/releases/getPerspectiveState.test.ts +3 -8
- package/src/releases/getPerspectiveState.ts +6 -19
- package/src/releases/observeReleases.test.ts +127 -0
- package/src/releases/observeReleases.ts +102 -0
- package/src/releases/releasesStore.test.ts +51 -53
- package/src/releases/releasesStore.ts +32 -32
- package/src/releases/utils/sortReleases.test.ts +36 -0
- package/src/releases/utils/sortReleases.ts +14 -9
|
@@ -497,6 +497,9 @@ describe('transitionAppliedTransactionsToOutgoing', () => {
|
|
|
497
497
|
expect(docState?.unverifiedRevisions).toBeDefined()
|
|
498
498
|
expect(docState?.unverifiedRevisions?.['txn7']).toBeDefined()
|
|
499
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'])
|
|
500
503
|
})
|
|
501
504
|
})
|
|
502
505
|
|
|
@@ -794,6 +797,399 @@ describe('applyRemoteDocument', () => {
|
|
|
794
797
|
expect(newDocState?.remote).toEqual(remote.document)
|
|
795
798
|
expect(newDocState?.remoteRev).toBe('currentRev')
|
|
796
799
|
})
|
|
800
|
+
|
|
801
|
+
describe('remote-patches events', () => {
|
|
802
|
+
const docId = getDraftId(DocumentId('doc1'))
|
|
803
|
+
|
|
804
|
+
function createInitialState(
|
|
805
|
+
unverifiedRevisions: NonNullable<
|
|
806
|
+
SyncTransactionState['documentStates'][string]
|
|
807
|
+
>['unverifiedRevisions'] = {},
|
|
808
|
+
recentOwnTransactionIds?: string[],
|
|
809
|
+
): SyncTransactionState {
|
|
810
|
+
return {
|
|
811
|
+
queued: [],
|
|
812
|
+
applied: [],
|
|
813
|
+
outgoing: undefined,
|
|
814
|
+
grants,
|
|
815
|
+
documentStates: {
|
|
816
|
+
[docId]: {
|
|
817
|
+
id: docId,
|
|
818
|
+
subscriptions: ['sub1'],
|
|
819
|
+
local: {...exampleDoc, _id: docId, foo: 'old'},
|
|
820
|
+
remote: {...exampleDoc, _id: docId, foo: 'old'},
|
|
821
|
+
unverifiedRevisions,
|
|
822
|
+
...(recentOwnTransactionIds && {recentOwnTransactionIds}),
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
it('emits a remote-patches event with origin "remote" for foreign transactions', () => {
|
|
829
|
+
const events = new Subject<DocumentEvent>()
|
|
830
|
+
const received: DocumentEvent[] = []
|
|
831
|
+
events.subscribe((e) => received.push(e))
|
|
832
|
+
|
|
833
|
+
const remote: RemoteDocument = {
|
|
834
|
+
type: 'mutation',
|
|
835
|
+
documentId: docId,
|
|
836
|
+
document: {...exampleDoc, _id: docId, foo: 'new'},
|
|
837
|
+
revision: 'txnForeign',
|
|
838
|
+
previousRev: 'txn0',
|
|
839
|
+
timestamp: '2025-02-06T00:12:00.000Z',
|
|
840
|
+
mutations: [
|
|
841
|
+
{
|
|
842
|
+
patch: {
|
|
843
|
+
id: docId,
|
|
844
|
+
unset: ['blocks[_key=="a"].children[_key=="b"].marks[0]'],
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
patch: {
|
|
849
|
+
id: docId,
|
|
850
|
+
insert: {after: 'blocks[_key=="a"].markDefs[-1]', items: [{_key: 'link1'}]},
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
],
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
applyRemoteDocument(createInitialState(), remote, events)
|
|
857
|
+
|
|
858
|
+
expect(received).toEqual([
|
|
859
|
+
{
|
|
860
|
+
type: 'remote-patches',
|
|
861
|
+
documentId: docId,
|
|
862
|
+
transactionId: 'txnForeign',
|
|
863
|
+
previousRev: 'txn0',
|
|
864
|
+
timestamp: '2025-02-06T00:12:00.000Z',
|
|
865
|
+
origin: 'remote',
|
|
866
|
+
patches: [
|
|
867
|
+
{unset: ['blocks[_key=="a"].children[_key=="b"].marks[0]']},
|
|
868
|
+
{insert: {after: 'blocks[_key=="a"].markDefs[-1]', items: [{_key: 'link1'}]}},
|
|
869
|
+
],
|
|
870
|
+
},
|
|
871
|
+
])
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
it('emits origin "local" when the transaction is one of our own unverified revisions', () => {
|
|
875
|
+
const events = new Subject<DocumentEvent>()
|
|
876
|
+
const received: DocumentEvent[] = []
|
|
877
|
+
events.subscribe((e) => received.push(e))
|
|
878
|
+
|
|
879
|
+
const remote: RemoteDocument = {
|
|
880
|
+
type: 'mutation',
|
|
881
|
+
documentId: docId,
|
|
882
|
+
document: {...exampleDoc, _id: docId, foo: 'new'},
|
|
883
|
+
revision: 'txnOwn',
|
|
884
|
+
previousRev: 'txn0',
|
|
885
|
+
timestamp: '2025-02-06T00:13:00.000Z',
|
|
886
|
+
mutations: [{patch: {id: docId, set: {foo: 'new'}}}],
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
applyRemoteDocument(
|
|
890
|
+
createInitialState({
|
|
891
|
+
txnOwn: {
|
|
892
|
+
transactionId: 'txnOwn',
|
|
893
|
+
documentId: docId,
|
|
894
|
+
previousRev: 'txn0',
|
|
895
|
+
timestamp: '2025-02-06T00:13:00.000Z',
|
|
896
|
+
},
|
|
897
|
+
}),
|
|
898
|
+
remote,
|
|
899
|
+
events,
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
expect(received).toEqual([
|
|
903
|
+
{
|
|
904
|
+
type: 'remote-patches',
|
|
905
|
+
documentId: docId,
|
|
906
|
+
transactionId: 'txnOwn',
|
|
907
|
+
previousRev: 'txn0',
|
|
908
|
+
timestamp: '2025-02-06T00:13:00.000Z',
|
|
909
|
+
origin: 'local',
|
|
910
|
+
patches: [{set: {foo: 'new'}}],
|
|
911
|
+
},
|
|
912
|
+
])
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
it('does not emit for sync events', () => {
|
|
916
|
+
const events = new Subject<DocumentEvent>()
|
|
917
|
+
const received: DocumentEvent[] = []
|
|
918
|
+
events.subscribe((e) => received.push(e))
|
|
919
|
+
|
|
920
|
+
const remote: RemoteDocument = {
|
|
921
|
+
type: 'sync',
|
|
922
|
+
documentId: docId,
|
|
923
|
+
document: {...exampleDoc, _id: docId, foo: 'new'},
|
|
924
|
+
revision: 'revSync',
|
|
925
|
+
timestamp: '2025-02-06T00:14:00.000Z',
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
applyRemoteDocument(createInitialState(), remote, events)
|
|
929
|
+
expect(received).toEqual([])
|
|
930
|
+
})
|
|
931
|
+
|
|
932
|
+
it('does not emit when the mutations contain no patches', () => {
|
|
933
|
+
const events = new Subject<DocumentEvent>()
|
|
934
|
+
const received: DocumentEvent[] = []
|
|
935
|
+
events.subscribe((e) => received.push(e))
|
|
936
|
+
|
|
937
|
+
const remote: RemoteDocument = {
|
|
938
|
+
type: 'mutation',
|
|
939
|
+
documentId: docId,
|
|
940
|
+
document: {...exampleDoc, _id: docId, foo: 'new'},
|
|
941
|
+
revision: 'txnCreate',
|
|
942
|
+
previousRev: 'txn0',
|
|
943
|
+
timestamp: '2025-02-06T00:15:00.000Z',
|
|
944
|
+
mutations: [{createOrReplace: {...exampleDoc, _id: docId, foo: 'new'}}],
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
applyRemoteDocument(createInitialState(), remote, events)
|
|
948
|
+
expect(received).toEqual([])
|
|
949
|
+
})
|
|
950
|
+
|
|
951
|
+
it('excludes patches addressed to other documents in the same transaction', () => {
|
|
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: 'txnMulti',
|
|
961
|
+
previousRev: 'txn0',
|
|
962
|
+
timestamp: '2025-02-06T00:20:00.000Z',
|
|
963
|
+
mutations: [
|
|
964
|
+
{patch: {id: docId, set: {foo: 'new'}}},
|
|
965
|
+
{patch: {id: 'some-other-doc', set: {foo: 'other'}}},
|
|
966
|
+
{patch: {query: '*[_type == "author"]', set: {foo: 'queried'}}},
|
|
967
|
+
],
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
applyRemoteDocument(createInitialState(), remote, events)
|
|
971
|
+
|
|
972
|
+
expect(received).toHaveLength(1)
|
|
973
|
+
expect((received[0] as {patches: unknown}).patches).toEqual([{set: {foo: 'new'}}])
|
|
974
|
+
})
|
|
975
|
+
|
|
976
|
+
it('labels origin "local" via recentOwnTransactionIds when the unverified revision was pruned', () => {
|
|
977
|
+
const events = new Subject<DocumentEvent>()
|
|
978
|
+
const received: DocumentEvent[] = []
|
|
979
|
+
events.subscribe((e) => received.push(e))
|
|
980
|
+
|
|
981
|
+
const remote: RemoteDocument = {
|
|
982
|
+
type: 'mutation',
|
|
983
|
+
documentId: docId,
|
|
984
|
+
document: {...exampleDoc, _id: docId, foo: 'new'},
|
|
985
|
+
revision: 'txnPruned',
|
|
986
|
+
previousRev: 'txn0',
|
|
987
|
+
timestamp: '2025-02-06T00:21:00.000Z',
|
|
988
|
+
mutations: [{patch: {id: docId, set: {foo: 'new'}}}],
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// a sync event racing the listener echo pruned the unverified revision,
|
|
992
|
+
// but the transaction ID is still tracked as one of our own
|
|
993
|
+
applyRemoteDocument(createInitialState({}, ['txnPruned']), remote, events)
|
|
994
|
+
|
|
995
|
+
expect(received).toHaveLength(1)
|
|
996
|
+
expect(received[0]).toMatchObject({
|
|
997
|
+
type: 'remote-patches',
|
|
998
|
+
transactionId: 'txnPruned',
|
|
999
|
+
origin: 'local',
|
|
1000
|
+
})
|
|
1001
|
+
})
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
describe('rebase with preserved operations', () => {
|
|
1005
|
+
it('skips a preserved-operations transaction that fails to re-apply and emits rebase-error', () => {
|
|
1006
|
+
const docId = getDraftId(DocumentId('doc1'))
|
|
1007
|
+
const originalDoc: SanityDocument = {
|
|
1008
|
+
...exampleDoc,
|
|
1009
|
+
_id: docId,
|
|
1010
|
+
_rev: 'rev1',
|
|
1011
|
+
title: 'The quick brown fox',
|
|
1012
|
+
}
|
|
1013
|
+
const locallyEditedDoc: SanityDocument = {
|
|
1014
|
+
...originalDoc,
|
|
1015
|
+
title: 'The quick brown cat',
|
|
1016
|
+
_rev: 'txnLocal',
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const appliedTransaction: AppliedTransaction = {
|
|
1020
|
+
transactionId: 'txnLocal',
|
|
1021
|
+
actions: [
|
|
1022
|
+
{
|
|
1023
|
+
type: 'document.edit',
|
|
1024
|
+
documentId: 'doc1',
|
|
1025
|
+
documentType: 'author',
|
|
1026
|
+
patches: [{diffMatchPatch: {title: '@@ -13,7 +13,7 @@\n own \n-fox\n+cat\n'}}],
|
|
1027
|
+
preserveOperations: true,
|
|
1028
|
+
},
|
|
1029
|
+
],
|
|
1030
|
+
previous: {[docId]: originalDoc},
|
|
1031
|
+
base: {[docId]: originalDoc},
|
|
1032
|
+
working: {[docId]: locallyEditedDoc},
|
|
1033
|
+
previousRevs: {[docId]: 'rev1'},
|
|
1034
|
+
timestamp: '2025-02-06T00:16:00.000Z',
|
|
1035
|
+
outgoingActions: [],
|
|
1036
|
+
outgoingMutations: [],
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const initialState: SyncTransactionState = {
|
|
1040
|
+
queued: [],
|
|
1041
|
+
applied: [appliedTransaction],
|
|
1042
|
+
outgoing: undefined,
|
|
1043
|
+
grants,
|
|
1044
|
+
documentStates: {
|
|
1045
|
+
[docId]: {
|
|
1046
|
+
id: docId,
|
|
1047
|
+
subscriptions: ['sub1'],
|
|
1048
|
+
local: locallyEditedDoc,
|
|
1049
|
+
remote: originalDoc,
|
|
1050
|
+
unverifiedRevisions: {},
|
|
1051
|
+
},
|
|
1052
|
+
},
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// a foreign transaction replaced the title with a non-string, so our
|
|
1056
|
+
// diffMatchPatch can no longer re-apply during the rebase
|
|
1057
|
+
const remoteDoc: SanityDocument = {
|
|
1058
|
+
...originalDoc,
|
|
1059
|
+
title: 42 as unknown as string,
|
|
1060
|
+
_rev: 'txnForeign',
|
|
1061
|
+
}
|
|
1062
|
+
const remote: RemoteDocument = {
|
|
1063
|
+
type: 'mutation',
|
|
1064
|
+
documentId: docId,
|
|
1065
|
+
document: remoteDoc,
|
|
1066
|
+
revision: 'txnForeign',
|
|
1067
|
+
previousRev: 'rev1',
|
|
1068
|
+
timestamp: '2025-02-06T00:17:00.000Z',
|
|
1069
|
+
mutations: [{patch: {id: docId, set: {title: 42}}}],
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const events = new Subject<DocumentEvent>()
|
|
1073
|
+
const received: DocumentEvent[] = []
|
|
1074
|
+
events.subscribe((e) => received.push(e))
|
|
1075
|
+
|
|
1076
|
+
const newState = applyRemoteDocument(initialState, remote, events)
|
|
1077
|
+
|
|
1078
|
+
const rebaseErrors = received.filter((e) => e.type === 'rebase-error')
|
|
1079
|
+
expect(rebaseErrors).toHaveLength(1)
|
|
1080
|
+
expect(rebaseErrors[0]).toMatchObject({
|
|
1081
|
+
type: 'rebase-error',
|
|
1082
|
+
transactionId: 'txnLocal',
|
|
1083
|
+
documentId: 'doc1',
|
|
1084
|
+
message: expect.stringMatching(/Failed to apply patches to the working document/),
|
|
1085
|
+
})
|
|
1086
|
+
|
|
1087
|
+
// the failing transaction is dropped and local converges to remote
|
|
1088
|
+
expect(newState.applied).toEqual([])
|
|
1089
|
+
expect(newState.documentStates[docId]?.local).toEqual(remoteDoc)
|
|
1090
|
+
expect(newState.documentStates[docId]?.remote).toEqual(remoteDoc)
|
|
1091
|
+
})
|
|
1092
|
+
|
|
1093
|
+
it('re-applies preserved keyed patches onto a diverged remote document', () => {
|
|
1094
|
+
const docId = getDraftId(DocumentId('doc1'))
|
|
1095
|
+
const originalDoc: SanityDocument = {
|
|
1096
|
+
...exampleDoc,
|
|
1097
|
+
_id: docId,
|
|
1098
|
+
_rev: 'rev1',
|
|
1099
|
+
items: [
|
|
1100
|
+
{_key: 'a', value: 'first'},
|
|
1101
|
+
{_key: 'b', value: 'second'},
|
|
1102
|
+
],
|
|
1103
|
+
}
|
|
1104
|
+
const locallyEditedDoc: SanityDocument = {
|
|
1105
|
+
...originalDoc,
|
|
1106
|
+
items: [
|
|
1107
|
+
{_key: 'a', value: 'first'},
|
|
1108
|
+
{_key: 'c', value: 'in between'},
|
|
1109
|
+
{_key: 'b', value: 'second'},
|
|
1110
|
+
],
|
|
1111
|
+
_rev: 'txnLocal',
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const appliedTransaction: AppliedTransaction = {
|
|
1115
|
+
transactionId: 'txnLocal',
|
|
1116
|
+
actions: [
|
|
1117
|
+
{
|
|
1118
|
+
type: 'document.edit',
|
|
1119
|
+
documentId: 'doc1',
|
|
1120
|
+
documentType: 'author',
|
|
1121
|
+
patches: [
|
|
1122
|
+
{insert: {after: 'items[_key=="a"]', items: [{_key: 'c', value: 'in between'}]}},
|
|
1123
|
+
],
|
|
1124
|
+
preserveOperations: true,
|
|
1125
|
+
},
|
|
1126
|
+
],
|
|
1127
|
+
previous: {[docId]: originalDoc},
|
|
1128
|
+
base: {[docId]: originalDoc},
|
|
1129
|
+
working: {[docId]: locallyEditedDoc},
|
|
1130
|
+
previousRevs: {[docId]: 'rev1'},
|
|
1131
|
+
timestamp: '2025-02-06T00:18:00.000Z',
|
|
1132
|
+
outgoingActions: [],
|
|
1133
|
+
outgoingMutations: [],
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const initialState: SyncTransactionState = {
|
|
1137
|
+
queued: [],
|
|
1138
|
+
applied: [appliedTransaction],
|
|
1139
|
+
outgoing: undefined,
|
|
1140
|
+
grants,
|
|
1141
|
+
documentStates: {
|
|
1142
|
+
[docId]: {
|
|
1143
|
+
id: docId,
|
|
1144
|
+
subscriptions: ['sub1'],
|
|
1145
|
+
local: locallyEditedDoc,
|
|
1146
|
+
remote: originalDoc,
|
|
1147
|
+
unverifiedRevisions: {},
|
|
1148
|
+
},
|
|
1149
|
+
},
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// a foreign transaction appended a new item concurrently
|
|
1153
|
+
const remoteDoc: SanityDocument = {
|
|
1154
|
+
...originalDoc,
|
|
1155
|
+
items: [
|
|
1156
|
+
{_key: 'a', value: 'first'},
|
|
1157
|
+
{_key: 'b', value: 'second'},
|
|
1158
|
+
{_key: 'd', value: 'appended remotely'},
|
|
1159
|
+
],
|
|
1160
|
+
_rev: 'txnForeign',
|
|
1161
|
+
}
|
|
1162
|
+
const remote: RemoteDocument = {
|
|
1163
|
+
type: 'mutation',
|
|
1164
|
+
documentId: docId,
|
|
1165
|
+
document: remoteDoc,
|
|
1166
|
+
revision: 'txnForeign',
|
|
1167
|
+
previousRev: 'rev1',
|
|
1168
|
+
timestamp: '2025-02-06T00:19:00.000Z',
|
|
1169
|
+
mutations: [
|
|
1170
|
+
{
|
|
1171
|
+
patch: {
|
|
1172
|
+
id: docId,
|
|
1173
|
+
insert: {after: 'items[-1]', items: [{_key: 'd', value: 'appended remotely'}]},
|
|
1174
|
+
},
|
|
1175
|
+
},
|
|
1176
|
+
],
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const events = new Subject<DocumentEvent>()
|
|
1180
|
+
const newState = applyRemoteDocument(initialState, remote, events)
|
|
1181
|
+
|
|
1182
|
+
// both edits survive: our keyed insert re-anchors after item "a" while
|
|
1183
|
+
// the remote append is preserved
|
|
1184
|
+
expect(newState.documentStates[docId]?.local?.['items']).toEqual([
|
|
1185
|
+
{_key: 'a', value: 'first'},
|
|
1186
|
+
{_key: 'c', value: 'in between'},
|
|
1187
|
+
{_key: 'b', value: 'second'},
|
|
1188
|
+
{_key: 'd', value: 'appended remotely'},
|
|
1189
|
+
])
|
|
1190
|
+
expect(newState.applied).toHaveLength(1)
|
|
1191
|
+
})
|
|
1192
|
+
})
|
|
797
1193
|
})
|
|
798
1194
|
|
|
799
1195
|
describe('addSubscriptionIdToDocument', () => {
|
package/src/document/reducers.ts
CHANGED
|
@@ -17,6 +17,13 @@ import {type DocumentSet} from './processMutations'
|
|
|
17
17
|
|
|
18
18
|
const EMPTY_REVISIONS: NonNullable<Required<DocumentState['unverifiedRevisions']>> = {}
|
|
19
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
|
+
|
|
20
27
|
export type SyncTransactionState = Pick<
|
|
21
28
|
DocumentStoreState,
|
|
22
29
|
'queued' | 'applied' | 'documentStates' | 'outgoing' | 'grants' | 'identity'
|
|
@@ -364,6 +371,15 @@ export function transitionAppliedTransactionsToOutgoing(
|
|
|
364
371
|
// add unverified revision
|
|
365
372
|
[transactionId]: {documentId, previousRev, transactionId, timestamp},
|
|
366
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
|
+
],
|
|
367
383
|
}
|
|
368
384
|
|
|
369
385
|
return acc
|
|
@@ -433,9 +449,29 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
|
|
|
433
449
|
}
|
|
434
450
|
}
|
|
435
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
|
+
|
|
436
472
|
export function applyRemoteDocument(
|
|
437
473
|
prev: SyncTransactionState,
|
|
438
|
-
{document, documentId, previousRev, revision, timestamp, type}: RemoteDocument,
|
|
474
|
+
{document, documentId, previousRev, revision, timestamp, type, mutations}: RemoteDocument,
|
|
439
475
|
events: DocumentStoreState['events'],
|
|
440
476
|
): SyncTransactionState {
|
|
441
477
|
if (!prev.grants) return prev
|
|
@@ -455,6 +491,31 @@ export function applyRemoteDocument(
|
|
|
455
491
|
unverifiedRevisions = omitProperty(prevUnverifiedRevisions, revision)
|
|
456
492
|
}
|
|
457
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
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
458
519
|
// if this remote document is from a `'sync'` event (meaning that the whole
|
|
459
520
|
// thing was just fetched and not re-created from mutations)
|
|
460
521
|
if (type === 'sync') {
|
|
@@ -43,6 +43,8 @@ export function createSharedListener(
|
|
|
43
43
|
{
|
|
44
44
|
events: ['mutation', 'welcome', 'reconnect'],
|
|
45
45
|
includeResult: false,
|
|
46
|
+
// the document store handles version documents, so we need to include all versions
|
|
47
|
+
includeAllVersions: true,
|
|
46
48
|
tag: 'document-listener',
|
|
47
49
|
// // from manual testing, it seems like mendoza patches may be
|
|
48
50
|
// // causing some ambiguity/wonkiness
|