@atlaskit/collab-provider 10.9.2 → 10.9.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @atlaskit/collab-provider
2
2
 
3
+ ## 10.9.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#122605](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/pull-requests/122605)
8
+ [`1bf1493f744ce`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/1bf1493f744ce) -
9
+ [ux] Add conflict metadata on reconnection
10
+ - Updated dependencies
11
+
3
12
  ## 10.9.2
4
13
 
5
14
  ### Patch Changes
@@ -203,7 +203,7 @@ var Channel = exports.Channel = /*#__PURE__*/function (_Emitter) {
203
203
  var measure = (0, _performance.stopMeasure)(_performance.MEASURE_NAME.DOCUMENT_INIT, _this.analyticsHelper);
204
204
  (_this$initExperience = _this.initExperience) === null || _this$initExperience === void 0 || _this$initExperience.success();
205
205
  (_this$analyticsHelper6 = _this.analyticsHelper) === null || _this$analyticsHelper6 === void 0 || _this$analyticsHelper6.sendActionEvent(_const.EVENT_ACTION.DOCUMENT_INIT,
206
- // TODO: detect when document init fails and fire corresponding event for it
206
+ // TODO: ED-26957 - detect when document init fails and fire corresponding event for it
207
207
  _const.EVENT_STATUS.SUCCESS, {
208
208
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
209
209
  resetReason: data.resetReason,
@@ -12,6 +12,7 @@ var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/creat
12
12
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
13
13
  var _throttle = _interopRequireDefault(require("lodash/throttle"));
14
14
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
15
+ var _transform = require("@atlaskit/editor-prosemirror/transform");
15
16
  var _prosemirrorCollab = require("@atlaskit/prosemirror-collab");
16
17
  var _state = require("@atlaskit/editor-prosemirror/state");
17
18
  var _editorJsonTransformer = require("@atlaskit/editor-json-transformer");
@@ -24,6 +25,7 @@ var _commitStep = require("../provider/commit-step");
24
25
  var _customErrors = require("../errors/custom-errors");
25
26
  var _catchupv = require("./catchupv2");
26
27
  var _stepQueueState = require("./step-queue-state");
28
+ var _getConflictChanges = require("./getConflictChanges");
27
29
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
28
30
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
29
31
  var CATCHUP_THROTTLE = 1 * 1000; // 1 second
@@ -72,7 +74,7 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
72
74
  return _this.catchupv2(reason, reconnectionMetadata);
73
75
  }, CATCHUP_THROTTLE, {
74
76
  leading: false,
75
- // TODO: why shouldn't this be leading?
77
+ // TODO: ED-26957 - why shouldn't this be leading?
76
78
  trailing: true
77
79
  }));
78
80
  /**
@@ -772,10 +774,21 @@ var DocumentService = exports.DocumentService = /*#__PURE__*/function () {
772
774
  var state = (_this$getState7 = this.getState) === null || _this$getState7 === void 0 ? void 0 : _this$getState7.call(this);
773
775
  var unconfirmedSteps = state ? (_getCollabState = (0, _prosemirrorCollab.getCollabState)(state)) === null || _getCollabState === void 0 ? void 0 : _getCollabState.unconfirmed : undefined;
774
776
  if (steps.length > 0 && state && unconfirmedSteps && unconfirmedSteps.length > 0) {
775
- // In the future we can determine the type of conflict
776
- this.providerEmitCallback('data:conflict', {
777
- offlineDoc: state.doc
777
+ var schema = state.schema,
778
+ tr = state.tr;
779
+ var remoteSteps = steps.map(function (s) {
780
+ return _transform.Step.fromJSON(schema, s);
778
781
  });
782
+ var conflicts = (0, _getConflictChanges.getConflictChanges)({
783
+ localSteps: unconfirmedSteps,
784
+ remoteSteps: remoteSteps,
785
+ tr: tr
786
+ });
787
+ if (conflicts.deleted.length > 0 || conflicts.inserted.length > 0) {
788
+ this.providerEmitCallback('data:conflict', _objectSpread({
789
+ offlineDoc: state.doc
790
+ }, conflicts));
791
+ }
779
792
  }
780
793
  }
781
794
  }, {
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.getConflictChanges = getConflictChanges;
8
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
+ var _prosemirrorChangeset = require("prosemirror-changeset");
10
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
11
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
12
+ var simplifySteps = function simplifySteps(steps) {
13
+ return steps.reduce(function (acc, step) {
14
+ var lastStep = acc[acc.length - 1];
15
+ if (lastStep) {
16
+ var mergedStep = lastStep.merge(step);
17
+ if (mergedStep) {
18
+ acc[acc.length - 1] = mergedStep;
19
+ return acc;
20
+ }
21
+ }
22
+ return acc.concat(step);
23
+ }, []);
24
+ };
25
+ function findContentChanges(doc, steps) {
26
+ var changes = _prosemirrorChangeset.ChangeSet.create(doc);
27
+ var latestDoc = doc;
28
+ simplifySteps(steps).forEach(function (step, index) {
29
+ var stepResult = step.apply(latestDoc);
30
+ if (stepResult.failed !== null || stepResult.doc === null) {
31
+ return;
32
+ }
33
+ latestDoc = stepResult.doc;
34
+ changes = changes.addSteps(latestDoc, [step.getMap()], {
35
+ step: index
36
+ });
37
+ });
38
+ return changes;
39
+ }
40
+
41
+ /**
42
+ * Iterate through the changesets to find overlapping regions that indicate conflicting
43
+ * changes
44
+ */
45
+ var getConflicts = function getConflicts(_ref) {
46
+ var localChanges = _ref.localChanges,
47
+ localDoc = _ref.localDoc,
48
+ remoteChanges = _ref.remoteChanges,
49
+ remoteDoc = _ref.remoteDoc;
50
+ var conflictingChanges = [];
51
+ localChanges.changes.forEach(function (localChange) {
52
+ remoteChanges.changes.forEach(function (remoteChange) {
53
+ if (
54
+ // Local change is inside remote change
55
+ localChange.fromA >= remoteChange.fromA && localChange.toA <= remoteChange.toA ||
56
+ // Remote change is inside local change
57
+ remoteChange.fromA >= localChange.fromA && remoteChange.toA <= localChange.toA ||
58
+ // Partial overlap with the end
59
+ localChange.fromA >= remoteChange.fromA && localChange.fromA < remoteChange.toA && localChange.toA > remoteChange.toA ||
60
+ // Partial overlap with the start
61
+ localChange.fromA < remoteChange.fromA && localChange.toA > remoteChange.fromA && localChange.toA <= remoteChange.toA) {
62
+ conflictingChanges.push({
63
+ from: Math.min(localChange.fromA, remoteChange.fromA),
64
+ to: Math.max(localChange.toA, remoteChange.toA),
65
+ local: localDoc.slice(localChange.fromB, localChange.toB, true),
66
+ remote: remoteDoc.slice(remoteChange.fromB, remoteChange.toB)
67
+ });
68
+ }
69
+ });
70
+ });
71
+ return conflictingChanges;
72
+ };
73
+
74
+ /**
75
+ * Almost a copy of the rebaseSteps in the collab algorithm (which gets called
76
+ * synchronously after this).
77
+ *
78
+ * This also tracks the intermediate documents so we can generate the changesets
79
+ * to use for finding any overlapping regions.
80
+ * See: `packages/editor/prosemirror-collab/src/index.ts`
81
+ */
82
+ var rebaseSteps = function rebaseSteps(_ref2) {
83
+ var _tr$mapping$maps;
84
+ var localSteps = _ref2.localSteps,
85
+ remoteSteps = _ref2.remoteSteps,
86
+ tr = _ref2.tr;
87
+ for (var i = (localSteps === null || localSteps === void 0 ? void 0 : localSteps.length) - 1; i >= 0; i--) {
88
+ tr.step(localSteps[i].inverted);
89
+ }
90
+ var originalDoc = tr.doc;
91
+ var mapStart = (_tr$mapping$maps = tr.mapping.maps) === null || _tr$mapping$maps === void 0 ? void 0 : _tr$mapping$maps.length;
92
+ for (var _i = 0; _i < remoteSteps.length; _i++) {
93
+ tr.step(remoteSteps[_i]);
94
+ }
95
+ var remoteDoc = tr.doc;
96
+ for (var _i2 = 0, mapFrom = localSteps.length; _i2 < localSteps.length; _i2++) {
97
+ var mapped = localSteps[_i2].step.map(tr.mapping.slice(mapFrom));
98
+ mapFrom--;
99
+ if (mapped && !tr.maybeStep(mapped).failed) {
100
+ // Open ticket for setMirror https://github.com/ProseMirror/prosemirror/issues/869
101
+ // @ts-expect-error
102
+ tr.mapping.setMirror(mapFrom, tr.steps.length - 1);
103
+ }
104
+ }
105
+ return {
106
+ mapStart: mapStart,
107
+ originalDoc: originalDoc,
108
+ remoteDoc: remoteDoc
109
+ };
110
+ };
111
+
112
+ /**
113
+ * Gets the conflicts between the local document and the remote document based on steps.
114
+ * It assumes the steps will be rebased using the `prosemirror-collab` algorithm synchronously after this
115
+ * Therefore the `tr` property is based on the document before rebasing.
116
+ *
117
+ * In the future we could possibly use `prosemirror-recreate-steps` (or similar approach)
118
+ * and tweak this to work for arbitrary diffs between offline and remote documents.
119
+ *
120
+ * @param localSteps Local steps applied between now and the server steps
121
+ * @param remoteSteps Steps retrieved from the server
122
+ * @param tr Transaction of the current document (expected to happen with local steps applied, before remote are applied)
123
+ * @returns All the conflicts (inserted + deleted) which can be applied to the current document
124
+ */
125
+ function getConflictChanges(_ref3) {
126
+ var localSteps = _ref3.localSteps,
127
+ remoteSteps = _ref3.remoteSteps,
128
+ tr = _ref3.tr;
129
+ var localDoc = tr.doc;
130
+ var _rebaseSteps = rebaseSteps({
131
+ localSteps: localSteps,
132
+ remoteSteps: remoteSteps,
133
+ tr: tr
134
+ }),
135
+ originalDoc = _rebaseSteps.originalDoc,
136
+ remoteDoc = _rebaseSteps.remoteDoc,
137
+ mapStart = _rebaseSteps.mapStart;
138
+ var localChanges = findContentChanges(originalDoc, localSteps.map(function (s) {
139
+ return s.step;
140
+ }));
141
+ var remoteChanges = findContentChanges(originalDoc, remoteSteps);
142
+
143
+ // This is the mapping between the original document and our final one which can be used to
144
+ // map conflict positions (which are based on the original doc)
145
+ var mapping = tr.mapping.slice(mapStart);
146
+
147
+ // Find the overlapping conflicts - these are based on the positions of the original document so are
148
+ // common to both local and remote documents.
149
+ // The above mapping allows us to bring these positions to where they are in the current document
150
+ var conflictingChanges = getConflicts({
151
+ localChanges: localChanges,
152
+ localDoc: localDoc,
153
+ remoteDoc: remoteDoc,
154
+ remoteChanges: remoteChanges
155
+ });
156
+ var isConflictChange = function isConflictChange(value) {
157
+ return Boolean(value);
158
+ };
159
+ return {
160
+ inserted: conflictingChanges.filter(function (i) {
161
+ return i.remote.size !== 0;
162
+ }).map(function (i) {
163
+ return _objectSpread(_objectSpread({}, i), {}, {
164
+ from: mapping.map(i.from, -1),
165
+ to: mapping.map(i.to)
166
+ });
167
+ }).filter(isConflictChange),
168
+ deleted: conflictingChanges.filter(function (d) {
169
+ return d.remote.size === 0;
170
+ }).map(function (d) {
171
+ return _objectSpread(_objectSpread({}, d), {}, {
172
+ from: mapping.map(d.from),
173
+ to: mapping.map(d.to)
174
+ });
175
+ }).filter(isConflictChange)
176
+ };
177
+ }
@@ -25,7 +25,7 @@ var NCS_ERROR_CODE = exports.NCS_ERROR_CODE = /*#__PURE__*/function (NCS_ERROR_C
25
25
  NCS_ERROR_CODE["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR";
26
26
  NCS_ERROR_CODE["PROSEMIRROR_SCHEMA_VALIDATION_ERROR"] = "PROSEMIRROR_SCHEMA_VALIDATION_ERROR";
27
27
  return NCS_ERROR_CODE;
28
- }({}); // TODO: Import emitted error codes from NCS
28
+ }({}); // TODO: ED-26957 - Import emitted error codes from NCS
29
29
  // NCS Errors
30
30
  // - Step rejection errors
31
31
  // - Permission errors
@@ -16,7 +16,7 @@ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbol
16
16
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
17
17
  var logger = (0, _utils.createLogger)('commit-step', 'black');
18
18
  var readyToCommit = exports.readyToCommit = true;
19
- var RESET_READYTOCOMMIT_INTERVAL_MS = exports.RESET_READYTOCOMMIT_INTERVAL_MS = 20000;
19
+ var RESET_READYTOCOMMIT_INTERVAL_MS = exports.RESET_READYTOCOMMIT_INTERVAL_MS = 5000;
20
20
  var commitStepQueue = exports.commitStepQueue = function commitStepQueue(_ref) {
21
21
  var broadcast = _ref.broadcast,
22
22
  steps = _ref.steps,
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.version = exports.nextMajorVersion = exports.name = void 0;
7
7
  var name = exports.name = "@atlaskit/collab-provider";
8
- var version = exports.version = "10.9.2";
8
+ var version = exports.version = "10.9.3";
9
9
  var nextMajorVersion = exports.nextMajorVersion = function nextMajorVersion() {
10
10
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
11
11
  };
@@ -139,7 +139,7 @@ export class Channel extends Emitter {
139
139
  const measure = stopMeasure(MEASURE_NAME.DOCUMENT_INIT, this.analyticsHelper);
140
140
  (_this$initExperience = this.initExperience) === null || _this$initExperience === void 0 ? void 0 : _this$initExperience.success();
141
141
  (_this$analyticsHelper6 = this.analyticsHelper) === null || _this$analyticsHelper6 === void 0 ? void 0 : _this$analyticsHelper6.sendActionEvent(EVENT_ACTION.DOCUMENT_INIT,
142
- // TODO: detect when document init fails and fire corresponding event for it
142
+ // TODO: ED-26957 - detect when document init fails and fire corresponding event for it
143
143
  EVENT_STATUS.SUCCESS, {
144
144
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
145
145
  resetReason: data.resetReason,
@@ -1,6 +1,7 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import throttle from 'lodash/throttle';
3
3
  import { fg } from '@atlaskit/platform-feature-flags';
4
+ import { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
4
5
  import { getCollabState, sendableSteps } from '@atlaskit/prosemirror-collab';
5
6
  import { Transaction } from '@atlaskit/editor-prosemirror/state';
6
7
  import { JSONTransformer } from '@atlaskit/editor-json-transformer';
@@ -13,6 +14,7 @@ import { commitStepQueue } from '../provider/commit-step';
13
14
  import { CantSyncUpError, UpdateDocumentError } from '../errors/custom-errors';
14
15
  import { catchupv2 } from './catchupv2';
15
16
  import { StepQueueState } from './step-queue-state';
17
+ import { getConflictChanges } from './getConflictChanges';
16
18
  const CATCHUP_THROTTLE = 1 * 1000; // 1 second
17
19
 
18
20
  const noop = () => {};
@@ -52,7 +54,7 @@ export class DocumentService {
52
54
  */
53
55
  _defineProperty(this, "throttledCatchupv2", throttle((reason, reconnectionMetadata) => this.catchupv2(reason, reconnectionMetadata), CATCHUP_THROTTLE, {
54
56
  leading: false,
55
- // TODO: why shouldn't this be leading?
57
+ // TODO: ED-26957 - why shouldn't this be leading?
56
58
  trailing: true
57
59
  }));
58
60
  /**
@@ -657,10 +659,22 @@ export class DocumentService {
657
659
  const state = (_this$getState7 = this.getState) === null || _this$getState7 === void 0 ? void 0 : _this$getState7.call(this);
658
660
  const unconfirmedSteps = state ? (_getCollabState = getCollabState(state)) === null || _getCollabState === void 0 ? void 0 : _getCollabState.unconfirmed : undefined;
659
661
  if (steps.length > 0 && state && unconfirmedSteps && unconfirmedSteps.length > 0) {
660
- // In the future we can determine the type of conflict
661
- this.providerEmitCallback('data:conflict', {
662
- offlineDoc: state.doc
662
+ const {
663
+ schema,
664
+ tr
665
+ } = state;
666
+ const remoteSteps = steps.map(s => ProseMirrorStep.fromJSON(schema, s));
667
+ const conflicts = getConflictChanges({
668
+ localSteps: unconfirmedSteps,
669
+ remoteSteps,
670
+ tr
663
671
  });
672
+ if (conflicts.deleted.length > 0 || conflicts.inserted.length > 0) {
673
+ this.providerEmitCallback('data:conflict', {
674
+ offlineDoc: state.doc,
675
+ ...conflicts
676
+ });
677
+ }
664
678
  }
665
679
  }
666
680
  processQueue() {
@@ -0,0 +1,161 @@
1
+ import { ChangeSet } from 'prosemirror-changeset';
2
+ const simplifySteps = steps => {
3
+ return steps.reduce((acc, step) => {
4
+ const lastStep = acc[acc.length - 1];
5
+ if (lastStep) {
6
+ const mergedStep = lastStep.merge(step);
7
+ if (mergedStep) {
8
+ acc[acc.length - 1] = mergedStep;
9
+ return acc;
10
+ }
11
+ }
12
+ return acc.concat(step);
13
+ }, []);
14
+ };
15
+ function findContentChanges(doc, steps) {
16
+ let changes = ChangeSet.create(doc);
17
+ let latestDoc = doc;
18
+ simplifySteps(steps).forEach((step, index) => {
19
+ const stepResult = step.apply(latestDoc);
20
+ if (stepResult.failed !== null || stepResult.doc === null) {
21
+ return;
22
+ }
23
+ latestDoc = stepResult.doc;
24
+ changes = changes.addSteps(latestDoc, [step.getMap()], {
25
+ step: index
26
+ });
27
+ });
28
+ return changes;
29
+ }
30
+
31
+ /**
32
+ * Iterate through the changesets to find overlapping regions that indicate conflicting
33
+ * changes
34
+ */
35
+ const getConflicts = ({
36
+ localChanges,
37
+ localDoc,
38
+ remoteChanges,
39
+ remoteDoc
40
+ }) => {
41
+ const conflictingChanges = [];
42
+ localChanges.changes.forEach(localChange => {
43
+ remoteChanges.changes.forEach(remoteChange => {
44
+ if (
45
+ // Local change is inside remote change
46
+ localChange.fromA >= remoteChange.fromA && localChange.toA <= remoteChange.toA ||
47
+ // Remote change is inside local change
48
+ remoteChange.fromA >= localChange.fromA && remoteChange.toA <= localChange.toA ||
49
+ // Partial overlap with the end
50
+ localChange.fromA >= remoteChange.fromA && localChange.fromA < remoteChange.toA && localChange.toA > remoteChange.toA ||
51
+ // Partial overlap with the start
52
+ localChange.fromA < remoteChange.fromA && localChange.toA > remoteChange.fromA && localChange.toA <= remoteChange.toA) {
53
+ conflictingChanges.push({
54
+ from: Math.min(localChange.fromA, remoteChange.fromA),
55
+ to: Math.max(localChange.toA, remoteChange.toA),
56
+ local: localDoc.slice(localChange.fromB, localChange.toB, true),
57
+ remote: remoteDoc.slice(remoteChange.fromB, remoteChange.toB)
58
+ });
59
+ }
60
+ });
61
+ });
62
+ return conflictingChanges;
63
+ };
64
+
65
+ /**
66
+ * Almost a copy of the rebaseSteps in the collab algorithm (which gets called
67
+ * synchronously after this).
68
+ *
69
+ * This also tracks the intermediate documents so we can generate the changesets
70
+ * to use for finding any overlapping regions.
71
+ * See: `packages/editor/prosemirror-collab/src/index.ts`
72
+ */
73
+ const rebaseSteps = ({
74
+ localSteps,
75
+ remoteSteps,
76
+ tr
77
+ }) => {
78
+ var _tr$mapping$maps;
79
+ for (let i = (localSteps === null || localSteps === void 0 ? void 0 : localSteps.length) - 1; i >= 0; i--) {
80
+ tr.step(localSteps[i].inverted);
81
+ }
82
+ const originalDoc = tr.doc;
83
+ const mapStart = (_tr$mapping$maps = tr.mapping.maps) === null || _tr$mapping$maps === void 0 ? void 0 : _tr$mapping$maps.length;
84
+ for (let i = 0; i < remoteSteps.length; i++) {
85
+ tr.step(remoteSteps[i]);
86
+ }
87
+ const remoteDoc = tr.doc;
88
+ for (let i = 0, mapFrom = localSteps.length; i < localSteps.length; i++) {
89
+ const mapped = localSteps[i].step.map(tr.mapping.slice(mapFrom));
90
+ mapFrom--;
91
+ if (mapped && !tr.maybeStep(mapped).failed) {
92
+ // Open ticket for setMirror https://github.com/ProseMirror/prosemirror/issues/869
93
+ // @ts-expect-error
94
+ tr.mapping.setMirror(mapFrom, tr.steps.length - 1);
95
+ }
96
+ }
97
+ return {
98
+ mapStart,
99
+ originalDoc,
100
+ remoteDoc
101
+ };
102
+ };
103
+
104
+ /**
105
+ * Gets the conflicts between the local document and the remote document based on steps.
106
+ * It assumes the steps will be rebased using the `prosemirror-collab` algorithm synchronously after this
107
+ * Therefore the `tr` property is based on the document before rebasing.
108
+ *
109
+ * In the future we could possibly use `prosemirror-recreate-steps` (or similar approach)
110
+ * and tweak this to work for arbitrary diffs between offline and remote documents.
111
+ *
112
+ * @param localSteps Local steps applied between now and the server steps
113
+ * @param remoteSteps Steps retrieved from the server
114
+ * @param tr Transaction of the current document (expected to happen with local steps applied, before remote are applied)
115
+ * @returns All the conflicts (inserted + deleted) which can be applied to the current document
116
+ */
117
+ export function getConflictChanges({
118
+ localSteps,
119
+ remoteSteps,
120
+ tr
121
+ }) {
122
+ const localDoc = tr.doc;
123
+ const {
124
+ originalDoc,
125
+ remoteDoc,
126
+ mapStart
127
+ } = rebaseSteps({
128
+ localSteps,
129
+ remoteSteps,
130
+ tr
131
+ });
132
+ const localChanges = findContentChanges(originalDoc, localSteps.map(s => s.step));
133
+ const remoteChanges = findContentChanges(originalDoc, remoteSteps);
134
+
135
+ // This is the mapping between the original document and our final one which can be used to
136
+ // map conflict positions (which are based on the original doc)
137
+ const mapping = tr.mapping.slice(mapStart);
138
+
139
+ // Find the overlapping conflicts - these are based on the positions of the original document so are
140
+ // common to both local and remote documents.
141
+ // The above mapping allows us to bring these positions to where they are in the current document
142
+ const conflictingChanges = getConflicts({
143
+ localChanges,
144
+ localDoc,
145
+ remoteDoc,
146
+ remoteChanges
147
+ });
148
+ const isConflictChange = value => Boolean(value);
149
+ return {
150
+ inserted: conflictingChanges.filter(i => i.remote.size !== 0).map(i => ({
151
+ ...i,
152
+ from: mapping.map(i.from, -1),
153
+ to: mapping.map(i.to)
154
+ })).filter(isConflictChange),
155
+ deleted: conflictingChanges.filter(d => d.remote.size === 0).map(d => ({
156
+ ...d,
157
+ from: mapping.map(d.from),
158
+ to: mapping.map(d.to)
159
+ })).filter(isConflictChange)
160
+ };
161
+ }
@@ -21,7 +21,7 @@ export let NCS_ERROR_CODE = /*#__PURE__*/function (NCS_ERROR_CODE) {
21
21
  return NCS_ERROR_CODE;
22
22
  }({});
23
23
 
24
- // TODO: Import emitted error codes from NCS
24
+ // TODO: ED-26957 - Import emitted error codes from NCS
25
25
 
26
26
  // NCS Errors
27
27
  // - Step rejection errors
@@ -6,7 +6,7 @@ import { NCS_ERROR_CODE } from '../errors/ncs-errors';
6
6
  import { createLogger } from '../helpers/utils';
7
7
  const logger = createLogger('commit-step', 'black');
8
8
  export let readyToCommit = true;
9
- export const RESET_READYTOCOMMIT_INTERVAL_MS = 20000;
9
+ export const RESET_READYTOCOMMIT_INTERVAL_MS = 5000;
10
10
  export const commitStepQueue = ({
11
11
  broadcast,
12
12
  steps,
@@ -1,5 +1,5 @@
1
1
  export const name = "@atlaskit/collab-provider";
2
- export const version = "10.9.2";
2
+ export const version = "10.9.3";
3
3
  export const nextMajorVersion = () => {
4
4
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
5
5
  };
@@ -196,7 +196,7 @@ export var Channel = /*#__PURE__*/function (_Emitter) {
196
196
  var measure = stopMeasure(MEASURE_NAME.DOCUMENT_INIT, _this.analyticsHelper);
197
197
  (_this$initExperience = _this.initExperience) === null || _this$initExperience === void 0 || _this$initExperience.success();
198
198
  (_this$analyticsHelper6 = _this.analyticsHelper) === null || _this$analyticsHelper6 === void 0 || _this$analyticsHelper6.sendActionEvent(EVENT_ACTION.DOCUMENT_INIT,
199
- // TODO: detect when document init fails and fire corresponding event for it
199
+ // TODO: ED-26957 - detect when document init fails and fire corresponding event for it
200
200
  EVENT_STATUS.SUCCESS, {
201
201
  latency: measure === null || measure === void 0 ? void 0 : measure.duration,
202
202
  resetReason: data.resetReason,
@@ -7,6 +7,7 @@ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbol
7
7
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
8
8
  import throttle from 'lodash/throttle';
9
9
  import { fg } from '@atlaskit/platform-feature-flags';
10
+ import { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
10
11
  import { getCollabState, sendableSteps } from '@atlaskit/prosemirror-collab';
11
12
  import { Transaction } from '@atlaskit/editor-prosemirror/state';
12
13
  import { JSONTransformer } from '@atlaskit/editor-json-transformer';
@@ -19,6 +20,7 @@ import { commitStepQueue } from '../provider/commit-step';
19
20
  import { CantSyncUpError, UpdateDocumentError } from '../errors/custom-errors';
20
21
  import { catchupv2 } from './catchupv2';
21
22
  import { StepQueueState } from './step-queue-state';
23
+ import { getConflictChanges } from './getConflictChanges';
22
24
  var CATCHUP_THROTTLE = 1 * 1000; // 1 second
23
25
 
24
26
  var noop = function noop() {};
@@ -65,7 +67,7 @@ export var DocumentService = /*#__PURE__*/function () {
65
67
  return _this.catchupv2(reason, reconnectionMetadata);
66
68
  }, CATCHUP_THROTTLE, {
67
69
  leading: false,
68
- // TODO: why shouldn't this be leading?
70
+ // TODO: ED-26957 - why shouldn't this be leading?
69
71
  trailing: true
70
72
  }));
71
73
  /**
@@ -765,10 +767,21 @@ export var DocumentService = /*#__PURE__*/function () {
765
767
  var state = (_this$getState7 = this.getState) === null || _this$getState7 === void 0 ? void 0 : _this$getState7.call(this);
766
768
  var unconfirmedSteps = state ? (_getCollabState = getCollabState(state)) === null || _getCollabState === void 0 ? void 0 : _getCollabState.unconfirmed : undefined;
767
769
  if (steps.length > 0 && state && unconfirmedSteps && unconfirmedSteps.length > 0) {
768
- // In the future we can determine the type of conflict
769
- this.providerEmitCallback('data:conflict', {
770
- offlineDoc: state.doc
770
+ var schema = state.schema,
771
+ tr = state.tr;
772
+ var remoteSteps = steps.map(function (s) {
773
+ return ProseMirrorStep.fromJSON(schema, s);
771
774
  });
775
+ var conflicts = getConflictChanges({
776
+ localSteps: unconfirmedSteps,
777
+ remoteSteps: remoteSteps,
778
+ tr: tr
779
+ });
780
+ if (conflicts.deleted.length > 0 || conflicts.inserted.length > 0) {
781
+ this.providerEmitCallback('data:conflict', _objectSpread({
782
+ offlineDoc: state.doc
783
+ }, conflicts));
784
+ }
772
785
  }
773
786
  }
774
787
  }, {
@@ -0,0 +1,170 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
+ import { ChangeSet } from 'prosemirror-changeset';
5
+ var simplifySteps = function simplifySteps(steps) {
6
+ return steps.reduce(function (acc, step) {
7
+ var lastStep = acc[acc.length - 1];
8
+ if (lastStep) {
9
+ var mergedStep = lastStep.merge(step);
10
+ if (mergedStep) {
11
+ acc[acc.length - 1] = mergedStep;
12
+ return acc;
13
+ }
14
+ }
15
+ return acc.concat(step);
16
+ }, []);
17
+ };
18
+ function findContentChanges(doc, steps) {
19
+ var changes = ChangeSet.create(doc);
20
+ var latestDoc = doc;
21
+ simplifySteps(steps).forEach(function (step, index) {
22
+ var stepResult = step.apply(latestDoc);
23
+ if (stepResult.failed !== null || stepResult.doc === null) {
24
+ return;
25
+ }
26
+ latestDoc = stepResult.doc;
27
+ changes = changes.addSteps(latestDoc, [step.getMap()], {
28
+ step: index
29
+ });
30
+ });
31
+ return changes;
32
+ }
33
+
34
+ /**
35
+ * Iterate through the changesets to find overlapping regions that indicate conflicting
36
+ * changes
37
+ */
38
+ var getConflicts = function getConflicts(_ref) {
39
+ var localChanges = _ref.localChanges,
40
+ localDoc = _ref.localDoc,
41
+ remoteChanges = _ref.remoteChanges,
42
+ remoteDoc = _ref.remoteDoc;
43
+ var conflictingChanges = [];
44
+ localChanges.changes.forEach(function (localChange) {
45
+ remoteChanges.changes.forEach(function (remoteChange) {
46
+ if (
47
+ // Local change is inside remote change
48
+ localChange.fromA >= remoteChange.fromA && localChange.toA <= remoteChange.toA ||
49
+ // Remote change is inside local change
50
+ remoteChange.fromA >= localChange.fromA && remoteChange.toA <= localChange.toA ||
51
+ // Partial overlap with the end
52
+ localChange.fromA >= remoteChange.fromA && localChange.fromA < remoteChange.toA && localChange.toA > remoteChange.toA ||
53
+ // Partial overlap with the start
54
+ localChange.fromA < remoteChange.fromA && localChange.toA > remoteChange.fromA && localChange.toA <= remoteChange.toA) {
55
+ conflictingChanges.push({
56
+ from: Math.min(localChange.fromA, remoteChange.fromA),
57
+ to: Math.max(localChange.toA, remoteChange.toA),
58
+ local: localDoc.slice(localChange.fromB, localChange.toB, true),
59
+ remote: remoteDoc.slice(remoteChange.fromB, remoteChange.toB)
60
+ });
61
+ }
62
+ });
63
+ });
64
+ return conflictingChanges;
65
+ };
66
+
67
+ /**
68
+ * Almost a copy of the rebaseSteps in the collab algorithm (which gets called
69
+ * synchronously after this).
70
+ *
71
+ * This also tracks the intermediate documents so we can generate the changesets
72
+ * to use for finding any overlapping regions.
73
+ * See: `packages/editor/prosemirror-collab/src/index.ts`
74
+ */
75
+ var rebaseSteps = function rebaseSteps(_ref2) {
76
+ var _tr$mapping$maps;
77
+ var localSteps = _ref2.localSteps,
78
+ remoteSteps = _ref2.remoteSteps,
79
+ tr = _ref2.tr;
80
+ for (var i = (localSteps === null || localSteps === void 0 ? void 0 : localSteps.length) - 1; i >= 0; i--) {
81
+ tr.step(localSteps[i].inverted);
82
+ }
83
+ var originalDoc = tr.doc;
84
+ var mapStart = (_tr$mapping$maps = tr.mapping.maps) === null || _tr$mapping$maps === void 0 ? void 0 : _tr$mapping$maps.length;
85
+ for (var _i = 0; _i < remoteSteps.length; _i++) {
86
+ tr.step(remoteSteps[_i]);
87
+ }
88
+ var remoteDoc = tr.doc;
89
+ for (var _i2 = 0, mapFrom = localSteps.length; _i2 < localSteps.length; _i2++) {
90
+ var mapped = localSteps[_i2].step.map(tr.mapping.slice(mapFrom));
91
+ mapFrom--;
92
+ if (mapped && !tr.maybeStep(mapped).failed) {
93
+ // Open ticket for setMirror https://github.com/ProseMirror/prosemirror/issues/869
94
+ // @ts-expect-error
95
+ tr.mapping.setMirror(mapFrom, tr.steps.length - 1);
96
+ }
97
+ }
98
+ return {
99
+ mapStart: mapStart,
100
+ originalDoc: originalDoc,
101
+ remoteDoc: remoteDoc
102
+ };
103
+ };
104
+
105
+ /**
106
+ * Gets the conflicts between the local document and the remote document based on steps.
107
+ * It assumes the steps will be rebased using the `prosemirror-collab` algorithm synchronously after this
108
+ * Therefore the `tr` property is based on the document before rebasing.
109
+ *
110
+ * In the future we could possibly use `prosemirror-recreate-steps` (or similar approach)
111
+ * and tweak this to work for arbitrary diffs between offline and remote documents.
112
+ *
113
+ * @param localSteps Local steps applied between now and the server steps
114
+ * @param remoteSteps Steps retrieved from the server
115
+ * @param tr Transaction of the current document (expected to happen with local steps applied, before remote are applied)
116
+ * @returns All the conflicts (inserted + deleted) which can be applied to the current document
117
+ */
118
+ export function getConflictChanges(_ref3) {
119
+ var localSteps = _ref3.localSteps,
120
+ remoteSteps = _ref3.remoteSteps,
121
+ tr = _ref3.tr;
122
+ var localDoc = tr.doc;
123
+ var _rebaseSteps = rebaseSteps({
124
+ localSteps: localSteps,
125
+ remoteSteps: remoteSteps,
126
+ tr: tr
127
+ }),
128
+ originalDoc = _rebaseSteps.originalDoc,
129
+ remoteDoc = _rebaseSteps.remoteDoc,
130
+ mapStart = _rebaseSteps.mapStart;
131
+ var localChanges = findContentChanges(originalDoc, localSteps.map(function (s) {
132
+ return s.step;
133
+ }));
134
+ var remoteChanges = findContentChanges(originalDoc, remoteSteps);
135
+
136
+ // This is the mapping between the original document and our final one which can be used to
137
+ // map conflict positions (which are based on the original doc)
138
+ var mapping = tr.mapping.slice(mapStart);
139
+
140
+ // Find the overlapping conflicts - these are based on the positions of the original document so are
141
+ // common to both local and remote documents.
142
+ // The above mapping allows us to bring these positions to where they are in the current document
143
+ var conflictingChanges = getConflicts({
144
+ localChanges: localChanges,
145
+ localDoc: localDoc,
146
+ remoteDoc: remoteDoc,
147
+ remoteChanges: remoteChanges
148
+ });
149
+ var isConflictChange = function isConflictChange(value) {
150
+ return Boolean(value);
151
+ };
152
+ return {
153
+ inserted: conflictingChanges.filter(function (i) {
154
+ return i.remote.size !== 0;
155
+ }).map(function (i) {
156
+ return _objectSpread(_objectSpread({}, i), {}, {
157
+ from: mapping.map(i.from, -1),
158
+ to: mapping.map(i.to)
159
+ });
160
+ }).filter(isConflictChange),
161
+ deleted: conflictingChanges.filter(function (d) {
162
+ return d.remote.size === 0;
163
+ }).map(function (d) {
164
+ return _objectSpread(_objectSpread({}, d), {}, {
165
+ from: mapping.map(d.from),
166
+ to: mapping.map(d.to)
167
+ });
168
+ }).filter(isConflictChange)
169
+ };
170
+ }
@@ -21,7 +21,7 @@ export var NCS_ERROR_CODE = /*#__PURE__*/function (NCS_ERROR_CODE) {
21
21
  return NCS_ERROR_CODE;
22
22
  }({});
23
23
 
24
- // TODO: Import emitted error codes from NCS
24
+ // TODO: ED-26957 - Import emitted error codes from NCS
25
25
 
26
26
  // NCS Errors
27
27
  // - Step rejection errors
@@ -9,7 +9,7 @@ import { NCS_ERROR_CODE } from '../errors/ncs-errors';
9
9
  import { createLogger } from '../helpers/utils';
10
10
  var logger = createLogger('commit-step', 'black');
11
11
  export var readyToCommit = true;
12
- export var RESET_READYTOCOMMIT_INTERVAL_MS = 20000;
12
+ export var RESET_READYTOCOMMIT_INTERVAL_MS = 5000;
13
13
  export var commitStepQueue = function commitStepQueue(_ref) {
14
14
  var broadcast = _ref.broadcast,
15
15
  steps = _ref.steps,
@@ -1,5 +1,5 @@
1
1
  export var name = "@atlaskit/collab-provider";
2
- export var version = "10.9.2";
2
+ export var version = "10.9.3";
3
3
  export var nextMajorVersion = function nextMajorVersion() {
4
4
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
5
5
  };
@@ -0,0 +1,26 @@
1
+ import { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
2
+ import { Transaction } from '@atlaskit/editor-prosemirror/state';
3
+ import type { ConflictChanges } from '@atlaskit/editor-common/collab';
4
+ interface Options {
5
+ localSteps: readonly {
6
+ inverted: ProseMirrorStep;
7
+ step: ProseMirrorStep;
8
+ }[];
9
+ remoteSteps: ProseMirrorStep[];
10
+ tr: Transaction;
11
+ }
12
+ /**
13
+ * Gets the conflicts between the local document and the remote document based on steps.
14
+ * It assumes the steps will be rebased using the `prosemirror-collab` algorithm synchronously after this
15
+ * Therefore the `tr` property is based on the document before rebasing.
16
+ *
17
+ * In the future we could possibly use `prosemirror-recreate-steps` (or similar approach)
18
+ * and tweak this to work for arbitrary diffs between offline and remote documents.
19
+ *
20
+ * @param localSteps Local steps applied between now and the server steps
21
+ * @param remoteSteps Steps retrieved from the server
22
+ * @param tr Transaction of the current document (expected to happen with local steps applied, before remote are applied)
23
+ * @returns All the conflicts (inserted + deleted) which can be applied to the current document
24
+ */
25
+ export declare function getConflictChanges({ localSteps, remoteSteps, tr }: Options): ConflictChanges;
26
+ export {};
@@ -4,7 +4,7 @@ import type { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/trans
4
4
  import type AnalyticsHelper from '../analytics/analytics-helper';
5
5
  import type { InternalError } from '../errors/internal-errors';
6
6
  export declare let readyToCommit: boolean;
7
- export declare const RESET_READYTOCOMMIT_INTERVAL_MS = 20000;
7
+ export declare const RESET_READYTOCOMMIT_INTERVAL_MS = 5000;
8
8
  export declare const commitStepQueue: ({ broadcast, steps, version, userId, clientId, onStepsAdded, onErrorHandled, analyticsHelper, emit, __livePage, hasRecovered, collabMode, forcePublish, }: {
9
9
  broadcast: <K extends keyof ChannelEvent>(type: K, data: Omit<ChannelEvent[K], 'timestamp'>, callback?: Function) => void;
10
10
  steps: readonly ProseMirrorStep[];
@@ -0,0 +1,26 @@
1
+ import { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/transform';
2
+ import { Transaction } from '@atlaskit/editor-prosemirror/state';
3
+ import type { ConflictChanges } from '@atlaskit/editor-common/collab';
4
+ interface Options {
5
+ localSteps: readonly {
6
+ inverted: ProseMirrorStep;
7
+ step: ProseMirrorStep;
8
+ }[];
9
+ remoteSteps: ProseMirrorStep[];
10
+ tr: Transaction;
11
+ }
12
+ /**
13
+ * Gets the conflicts between the local document and the remote document based on steps.
14
+ * It assumes the steps will be rebased using the `prosemirror-collab` algorithm synchronously after this
15
+ * Therefore the `tr` property is based on the document before rebasing.
16
+ *
17
+ * In the future we could possibly use `prosemirror-recreate-steps` (or similar approach)
18
+ * and tweak this to work for arbitrary diffs between offline and remote documents.
19
+ *
20
+ * @param localSteps Local steps applied between now and the server steps
21
+ * @param remoteSteps Steps retrieved from the server
22
+ * @param tr Transaction of the current document (expected to happen with local steps applied, before remote are applied)
23
+ * @returns All the conflicts (inserted + deleted) which can be applied to the current document
24
+ */
25
+ export declare function getConflictChanges({ localSteps, remoteSteps, tr }: Options): ConflictChanges;
26
+ export {};
@@ -4,7 +4,7 @@ import type { Step as ProseMirrorStep } from '@atlaskit/editor-prosemirror/trans
4
4
  import type AnalyticsHelper from '../analytics/analytics-helper';
5
5
  import type { InternalError } from '../errors/internal-errors';
6
6
  export declare let readyToCommit: boolean;
7
- export declare const RESET_READYTOCOMMIT_INTERVAL_MS = 20000;
7
+ export declare const RESET_READYTOCOMMIT_INTERVAL_MS = 5000;
8
8
  export declare const commitStepQueue: ({ broadcast, steps, version, userId, clientId, onStepsAdded, onErrorHandled, analyticsHelper, emit, __livePage, hasRecovered, collabMode, forcePublish, }: {
9
9
  broadcast: <K extends keyof ChannelEvent>(type: K, data: Omit<ChannelEvent[K], 'timestamp'>, callback?: Function) => void;
10
10
  steps: readonly ProseMirrorStep[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/collab-provider",
3
- "version": "10.9.2",
3
+ "version": "10.9.3",
4
4
  "description": "A provider for collaborative editing.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -35,7 +35,7 @@
35
35
  "@atlaskit/adf-utils": "^19.19.0",
36
36
  "@atlaskit/analytics-gas-types": "^5.1.0",
37
37
  "@atlaskit/analytics-listeners": "^9.0.0",
38
- "@atlaskit/editor-common": "^102.0.0",
38
+ "@atlaskit/editor-common": "^102.2.0",
39
39
  "@atlaskit/editor-json-transformer": "^8.24.0",
40
40
  "@atlaskit/editor-prosemirror": "7.0.0",
41
41
  "@atlaskit/feature-gate-js-client": "^4.26.0",
@@ -47,6 +47,7 @@
47
47
  "@babel/runtime": "^7.0.0",
48
48
  "eventemitter2": "^4.1.0",
49
49
  "lodash": "^4.17.21",
50
+ "prosemirror-changeset": "^2.2.1",
50
51
  "socket.io-client": "^4.7.5",
51
52
  "uuid": "^3.1.0"
52
53
  },