@atlaskit/editor-synced-block-provider 3.27.2 → 3.28.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @atlaskit/editor-synced-block-provider
2
2
 
3
+ ## 3.28.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`2d04d83eba130`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/2d04d83eba130) -
8
+ EDITOR-4997 update cache dirty logic to reduce request
9
+
3
10
  ## 3.27.2
4
11
 
5
12
  ### Patch Changes
@@ -11,6 +11,7 @@ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/
11
11
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
12
12
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
13
13
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
14
+ var _isEqual = _interopRequireDefault(require("lodash/isEqual"));
14
15
  var _rafSchd = _interopRequireDefault(require("raf-schd"));
15
16
  var _coreUtils = require("@atlaskit/editor-common/core-utils");
16
17
  var _monitoring = require("@atlaskit/editor-common/monitoring");
@@ -43,6 +44,12 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
43
44
  (0, _defineProperty2.default)(this, "isRefreshingSubscriptions", false);
44
45
  // Flag to indicate if real-time subscriptions are enabled
45
46
  (0, _defineProperty2.default)(this, "useRealTimeSubscriptions", false);
47
+ // Keep track of the last flushed subscriptions to optimize cache flushing on document save
48
+ (0, _defineProperty2.default)(this, "lastFlushedSyncedBlocks", {});
49
+ // Track if a flush operation is currently in progress
50
+ (0, _defineProperty2.default)(this, "isFlushInProgress", false);
51
+ // Track if another flush is needed after the current one completes
52
+ (0, _defineProperty2.default)(this, "flushNeededAfterCurrent", false);
46
53
  (0, _defineProperty2.default)(this, "pendingFetchRequests", new Set());
47
54
  (0, _defineProperty2.default)(this, "scheduledBatchFetch", (0, _rafSchd.default)(function () {
48
55
  if (_this.pendingFetchRequests.size === 0) {
@@ -1160,19 +1167,104 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
1160
1167
  key: "flush",
1161
1168
  value: (function () {
1162
1169
  var _flush = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4() {
1163
- var success, _this$saveExperience, blocks, updateResult, _this$saveExperience2, _this$fireAnalyticsEv1, _this$saveExperience3, _this$fireAnalyticsEv10, _this$saveExperience4;
1164
- return _regenerator.default.wrap(function _callee4$(_context5) {
1165
- while (1) switch (_context5.prev = _context5.next) {
1170
+ var _this10 = this;
1171
+ var success, syncedBlocksToFlush, _this$saveExperience, blocks, _iterator4, _step4, _loop2, updateResult, _this$saveExperience2, _this$fireAnalyticsEv1, _this$saveExperience3, _this$fireAnalyticsEv10, _this$saveExperience4;
1172
+ return _regenerator.default.wrap(function _callee4$(_context6) {
1173
+ while (1) switch (_context6.prev = _context6.next) {
1166
1174
  case 0:
1167
1175
  if (this.isCacheDirty) {
1168
- _context5.next = 2;
1176
+ _context6.next = 2;
1169
1177
  break;
1170
1178
  }
1171
- return _context5.abrupt("return", true);
1179
+ return _context6.abrupt("return", true);
1172
1180
  case 2:
1173
- success = true;
1174
- _context5.prev = 3;
1175
- blocks = []; // Collect all reference synced blocks on the current document
1181
+ if (!(0, _platformFeatureFlags.fg)('platform_synced_block_patch_2')) {
1182
+ _context6.next = 9;
1183
+ break;
1184
+ }
1185
+ if (!this.isFlushInProgress) {
1186
+ _context6.next = 8;
1187
+ break;
1188
+ }
1189
+ // Mark that another flush is needed after the current one completes
1190
+ this.flushNeededAfterCurrent = true;
1191
+
1192
+ // We return true here because we know the pending flush will handle the dirty cache
1193
+ return _context6.abrupt("return", true);
1194
+ case 8:
1195
+ this.isFlushInProgress = true;
1196
+ case 9:
1197
+ success = true; // a copy of the subscriptions STRUCTURE (without the callbacks)
1198
+ // To be saved as the last flushed structure if the flush is successful
1199
+ syncedBlocksToFlush = {};
1200
+ _context6.prev = 11;
1201
+ if (this.dataProvider) {
1202
+ _context6.next = 14;
1203
+ break;
1204
+ }
1205
+ throw new Error('Data provider not set');
1206
+ case 14:
1207
+ blocks = [];
1208
+ if (!(0, _platformFeatureFlags.fg)('platform_synced_block_patch_2')) {
1209
+ _context6.next = 37;
1210
+ break;
1211
+ }
1212
+ // First, build the complete subscription structure
1213
+ _iterator4 = _createForOfIteratorHelper(this.subscriptions.entries());
1214
+ _context6.prev = 17;
1215
+ _loop2 = /*#__PURE__*/_regenerator.default.mark(function _loop2() {
1216
+ var _step4$value, resourceId, callbacks;
1217
+ return _regenerator.default.wrap(function _loop2$(_context5) {
1218
+ while (1) switch (_context5.prev = _context5.next) {
1219
+ case 0:
1220
+ _step4$value = (0, _slicedToArray2.default)(_step4.value, 2), resourceId = _step4$value[0], callbacks = _step4$value[1];
1221
+ syncedBlocksToFlush[resourceId] = {};
1222
+ Object.keys(callbacks).forEach(function (localId) {
1223
+ blocks.push({
1224
+ resourceId: resourceId,
1225
+ localId: localId
1226
+ });
1227
+ syncedBlocksToFlush[resourceId][localId] = true;
1228
+ });
1229
+ case 3:
1230
+ case "end":
1231
+ return _context5.stop();
1232
+ }
1233
+ }, _loop2);
1234
+ });
1235
+ _iterator4.s();
1236
+ case 20:
1237
+ if ((_step4 = _iterator4.n()).done) {
1238
+ _context6.next = 24;
1239
+ break;
1240
+ }
1241
+ return _context6.delegateYield(_loop2(), "t0", 22);
1242
+ case 22:
1243
+ _context6.next = 20;
1244
+ break;
1245
+ case 24:
1246
+ _context6.next = 29;
1247
+ break;
1248
+ case 26:
1249
+ _context6.prev = 26;
1250
+ _context6.t1 = _context6["catch"](17);
1251
+ _iterator4.e(_context6.t1);
1252
+ case 29:
1253
+ _context6.prev = 29;
1254
+ _iterator4.f();
1255
+ return _context6.finish(29);
1256
+ case 32:
1257
+ if (!(0, _isEqual.default)(syncedBlocksToFlush, this.lastFlushedSyncedBlocks)) {
1258
+ _context6.next = 35;
1259
+ break;
1260
+ }
1261
+ this.isCacheDirty = false; // Reset since we're considering this a successful no-op flush
1262
+ return _context6.abrupt("return", true);
1263
+ case 35:
1264
+ _context6.next = 38;
1265
+ break;
1266
+ case 37:
1267
+ // Collect all reference synced blocks on the current document
1176
1268
  Array.from(this.subscriptions.entries()).forEach(function (_ref2) {
1177
1269
  var _ref3 = (0, _slicedToArray2.default)(_ref2, 2),
1178
1270
  resourceId = _ref3[0],
@@ -1184,12 +1276,7 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
1184
1276
  });
1185
1277
  });
1186
1278
  });
1187
- if (this.dataProvider) {
1188
- _context5.next = 8;
1189
- break;
1190
- }
1191
- throw new Error('Data provider not set');
1192
- case 8:
1279
+ case 38:
1193
1280
  // reset isCacheDirty early to prevent race condition
1194
1281
  // There is a race condition where if a user makes changes (create/delete) to a reference sync block
1195
1282
  // on a live page and the reference sync block is being saved while the user
@@ -1197,10 +1284,10 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
1197
1284
  // exactly at a time when the updateReferenceData is being executed asynchronously.
1198
1285
  this.isCacheDirty = false;
1199
1286
  (_this$saveExperience = this.saveExperience) === null || _this$saveExperience === void 0 || _this$saveExperience.start();
1200
- _context5.next = 12;
1287
+ _context6.next = 42;
1201
1288
  return this.dataProvider.updateReferenceData(blocks);
1202
- case 12:
1203
- updateResult = _context5.sent;
1289
+ case 42:
1290
+ updateResult = _context6.sent;
1204
1291
  if (!updateResult.success) {
1205
1292
  success = false;
1206
1293
  (_this$saveExperience2 = this.saveExperience) === null || _this$saveExperience2 === void 0 || _this$saveExperience2.failure({
@@ -1208,35 +1295,53 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
1208
1295
  });
1209
1296
  (_this$fireAnalyticsEv1 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv1 === void 0 || _this$fireAnalyticsEv1.call(this, (0, _errorHandling.updateReferenceErrorPayload)(updateResult.error || 'Failed to update reference synced blocks on the document'));
1210
1297
  }
1211
- _context5.next = 22;
1298
+ _context6.next = 52;
1212
1299
  break;
1213
- case 16:
1214
- _context5.prev = 16;
1215
- _context5.t0 = _context5["catch"](3);
1300
+ case 46:
1301
+ _context6.prev = 46;
1302
+ _context6.t2 = _context6["catch"](11);
1216
1303
  success = false;
1217
- (0, _monitoring.logException)(_context5.t0, {
1304
+ (0, _monitoring.logException)(_context6.t2, {
1218
1305
  location: 'editor-synced-block-provider/referenceSyncBlockStoreManager'
1219
1306
  });
1220
1307
  (_this$saveExperience3 = this.saveExperience) === null || _this$saveExperience3 === void 0 || _this$saveExperience3.failure({
1221
- reason: _context5.t0.message
1308
+ reason: _context6.t2.message
1222
1309
  });
1223
- (_this$fireAnalyticsEv10 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv10 === void 0 || _this$fireAnalyticsEv10.call(this, (0, _errorHandling.updateReferenceErrorPayload)(_context5.t0.message));
1224
- case 22:
1225
- _context5.prev = 22;
1310
+ (_this$fireAnalyticsEv10 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv10 === void 0 || _this$fireAnalyticsEv10.call(this, (0, _errorHandling.updateReferenceErrorPayload)(_context6.t2.message));
1311
+ case 52:
1312
+ _context6.prev = 52;
1226
1313
  if (!success) {
1227
1314
  // set isCacheDirty back to true for cases where it failed to update the reference synced blocks on the BE
1228
1315
  this.isCacheDirty = true;
1229
1316
  } else {
1317
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_2')) {
1318
+ this.lastFlushedSyncedBlocks = syncedBlocksToFlush;
1319
+ }
1230
1320
  (_this$saveExperience4 = this.saveExperience) === null || _this$saveExperience4 === void 0 || _this$saveExperience4.success();
1231
1321
  }
1232
- return _context5.finish(22);
1233
- case 25:
1234
- return _context5.abrupt("return", success);
1235
- case 26:
1322
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_2')) {
1323
+ // Always reset isFlushInProgress regardless of feature flag
1324
+ this.isFlushInProgress = false;
1325
+
1326
+ // If another flush was requested while this one was in progress, execute it now
1327
+ if (this.flushNeededAfterCurrent) {
1328
+ this.flushNeededAfterCurrent = false;
1329
+ // Use setTimeout to avoid deep recursion and run queued flush asynchronously
1330
+ // Note: flush() handles all exceptions internally and never rejects
1331
+ this.queuedFlushTimeout = setTimeout(function () {
1332
+ _this10.queuedFlushTimeout = undefined;
1333
+ void _this10.flush();
1334
+ }, 0);
1335
+ }
1336
+ }
1337
+ return _context6.finish(52);
1338
+ case 56:
1339
+ return _context6.abrupt("return", success);
1340
+ case 57:
1236
1341
  case "end":
1237
- return _context5.stop();
1342
+ return _context6.stop();
1238
1343
  }
1239
- }, _callee4, this, [[3, 16, 22, 25]]);
1344
+ }, _callee4, this, [[11, 46, 52, 56], [17, 26, 29, 32]]);
1240
1345
  }));
1241
1346
  function flush() {
1242
1347
  return _flush.apply(this, arguments);
@@ -1247,6 +1352,12 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
1247
1352
  key: "destroy",
1248
1353
  value: function destroy() {
1249
1354
  var _this$saveExperience5, _this$fetchExperience0, _this$fetchSourceInfo2;
1355
+ // Cancel any queued flush to prevent it from running after destroy
1356
+ if (this.queuedFlushTimeout) {
1357
+ clearTimeout(this.queuedFlushTimeout);
1358
+ this.queuedFlushTimeout = undefined;
1359
+ }
1360
+
1250
1361
  // Clean up all GraphQL subscriptions first
1251
1362
  this.cleanupAllGraphQLSubscriptions();
1252
1363
  if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_1')) {
@@ -1,4 +1,5 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import isEqual from 'lodash/isEqual';
2
3
  import rafSchedule from 'raf-schd';
3
4
  import { isSSR } from '@atlaskit/editor-common/core-utils';
4
5
  import { logException } from '@atlaskit/editor-common/monitoring';
@@ -25,6 +26,12 @@ export class ReferenceSyncBlockStoreManager {
25
26
  _defineProperty(this, "isRefreshingSubscriptions", false);
26
27
  // Flag to indicate if real-time subscriptions are enabled
27
28
  _defineProperty(this, "useRealTimeSubscriptions", false);
29
+ // Keep track of the last flushed subscriptions to optimize cache flushing on document save
30
+ _defineProperty(this, "lastFlushedSyncedBlocks", {});
31
+ // Track if a flush operation is currently in progress
32
+ _defineProperty(this, "isFlushInProgress", false);
33
+ // Track if another flush is needed after the current one completes
34
+ _defineProperty(this, "flushNeededAfterCurrent", false);
28
35
  _defineProperty(this, "pendingFetchRequests", new Set());
29
36
  _defineProperty(this, "scheduledBatchFetch", rafSchedule(() => {
30
37
  if (this.pendingFetchRequests.size === 0) {
@@ -960,24 +967,63 @@ export class ReferenceSyncBlockStoreManager {
960
967
  */
961
968
  async flush() {
962
969
  if (!this.isCacheDirty) {
970
+ // we use the isCacheDirty flag as a quick check.
963
971
  return true;
964
972
  }
973
+
974
+ // Prevent concurrent flushes to avoid race conditions with lastFlushedSyncedBlocks
975
+ if (fg('platform_synced_block_patch_2')) {
976
+ if (this.isFlushInProgress) {
977
+ // Mark that another flush is needed after the current one completes
978
+ this.flushNeededAfterCurrent = true;
979
+
980
+ // We return true here because we know the pending flush will handle the dirty cache
981
+ return true;
982
+ } else {
983
+ this.isFlushInProgress = true;
984
+ }
985
+ }
965
986
  let success = true;
987
+ // a copy of the subscriptions STRUCTURE (without the callbacks)
988
+ // To be saved as the last flushed structure if the flush is successful
989
+ const syncedBlocksToFlush = {};
966
990
  try {
967
991
  var _this$saveExperience;
992
+ if (!this.dataProvider) {
993
+ throw new Error('Data provider not set');
994
+ }
968
995
  const blocks = [];
996
+ if (fg('platform_synced_block_patch_2')) {
997
+ // First, build the complete subscription structure
998
+ for (const [resourceId, callbacks] of this.subscriptions.entries()) {
999
+ syncedBlocksToFlush[resourceId] = {};
1000
+ Object.keys(callbacks).forEach(localId => {
1001
+ blocks.push({
1002
+ resourceId,
1003
+ localId
1004
+ });
1005
+ syncedBlocksToFlush[resourceId][localId] = true;
1006
+ });
1007
+ }
969
1008
 
970
- // Collect all reference synced blocks on the current document
971
- Array.from(this.subscriptions.entries()).forEach(([resourceId, callbacks]) => {
972
- Object.keys(callbacks).forEach(localId => {
973
- blocks.push({
974
- resourceId,
975
- localId
1009
+ // Then, compare with the last flushed structure to detect changes
1010
+ // We check against the last flushed structure to prevent unnecessary flushes
1011
+ // Note that we will always flush at least once when editor starts
1012
+ // This is useful for eventual consistency between the editor and the BE.
1013
+ if (isEqual(syncedBlocksToFlush, this.lastFlushedSyncedBlocks)) {
1014
+ this.isCacheDirty = false; // Reset since we're considering this a successful no-op flush
1015
+ return true;
1016
+ }
1017
+ } else {
1018
+ // Collect all reference synced blocks on the current document
1019
+ Array.from(this.subscriptions.entries()).forEach(([resourceId, callbacks]) => {
1020
+ Object.keys(callbacks).forEach(localId => {
1021
+ blocks.push({
1022
+ resourceId,
1023
+ localId
1024
+ });
976
1025
  });
977
1026
  });
978
- });
979
- if (!this.dataProvider) {
980
- throw new Error('Data provider not set');
981
1027
  }
982
1028
 
983
1029
  // reset isCacheDirty early to prevent race condition
@@ -1012,13 +1058,37 @@ export class ReferenceSyncBlockStoreManager {
1012
1058
  this.isCacheDirty = true;
1013
1059
  } else {
1014
1060
  var _this$saveExperience4;
1061
+ if (fg('platform_synced_block_patch_2')) {
1062
+ this.lastFlushedSyncedBlocks = syncedBlocksToFlush;
1063
+ }
1015
1064
  (_this$saveExperience4 = this.saveExperience) === null || _this$saveExperience4 === void 0 ? void 0 : _this$saveExperience4.success();
1016
1065
  }
1066
+ if (fg('platform_synced_block_patch_2')) {
1067
+ // Always reset isFlushInProgress regardless of feature flag
1068
+ this.isFlushInProgress = false;
1069
+
1070
+ // If another flush was requested while this one was in progress, execute it now
1071
+ if (this.flushNeededAfterCurrent) {
1072
+ this.flushNeededAfterCurrent = false;
1073
+ // Use setTimeout to avoid deep recursion and run queued flush asynchronously
1074
+ // Note: flush() handles all exceptions internally and never rejects
1075
+ this.queuedFlushTimeout = setTimeout(() => {
1076
+ this.queuedFlushTimeout = undefined;
1077
+ void this.flush();
1078
+ }, 0);
1079
+ }
1080
+ }
1017
1081
  }
1018
1082
  return success;
1019
1083
  }
1020
1084
  destroy() {
1021
1085
  var _this$saveExperience5, _this$fetchExperience0, _this$fetchSourceInfo6;
1086
+ // Cancel any queued flush to prevent it from running after destroy
1087
+ if (this.queuedFlushTimeout) {
1088
+ clearTimeout(this.queuedFlushTimeout);
1089
+ this.queuedFlushTimeout = undefined;
1090
+ }
1091
+
1022
1092
  // Clean up all GraphQL subscriptions first
1023
1093
  this.cleanupAllGraphQLSubscriptions();
1024
1094
  if (fg('platform_synced_block_patch_1')) {
@@ -9,6 +9,7 @@ import _regeneratorRuntime from "@babel/runtime/regenerator";
9
9
  function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
10
10
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
11
11
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
12
+ import isEqual from 'lodash/isEqual';
12
13
  import rafSchedule from 'raf-schd';
13
14
  import { isSSR } from '@atlaskit/editor-common/core-utils';
14
15
  import { logException } from '@atlaskit/editor-common/monitoring';
@@ -37,6 +38,12 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
37
38
  _defineProperty(this, "isRefreshingSubscriptions", false);
38
39
  // Flag to indicate if real-time subscriptions are enabled
39
40
  _defineProperty(this, "useRealTimeSubscriptions", false);
41
+ // Keep track of the last flushed subscriptions to optimize cache flushing on document save
42
+ _defineProperty(this, "lastFlushedSyncedBlocks", {});
43
+ // Track if a flush operation is currently in progress
44
+ _defineProperty(this, "isFlushInProgress", false);
45
+ // Track if another flush is needed after the current one completes
46
+ _defineProperty(this, "flushNeededAfterCurrent", false);
40
47
  _defineProperty(this, "pendingFetchRequests", new Set());
41
48
  _defineProperty(this, "scheduledBatchFetch", rafSchedule(function () {
42
49
  if (_this.pendingFetchRequests.size === 0) {
@@ -1154,19 +1161,104 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
1154
1161
  key: "flush",
1155
1162
  value: (function () {
1156
1163
  var _flush = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee4() {
1157
- var success, _this$saveExperience, blocks, updateResult, _this$saveExperience2, _this$fireAnalyticsEv1, _this$saveExperience3, _this$fireAnalyticsEv10, _this$saveExperience4;
1158
- return _regeneratorRuntime.wrap(function _callee4$(_context5) {
1159
- while (1) switch (_context5.prev = _context5.next) {
1164
+ var _this10 = this;
1165
+ var success, syncedBlocksToFlush, _this$saveExperience, blocks, _iterator4, _step4, _loop2, updateResult, _this$saveExperience2, _this$fireAnalyticsEv1, _this$saveExperience3, _this$fireAnalyticsEv10, _this$saveExperience4;
1166
+ return _regeneratorRuntime.wrap(function _callee4$(_context6) {
1167
+ while (1) switch (_context6.prev = _context6.next) {
1160
1168
  case 0:
1161
1169
  if (this.isCacheDirty) {
1162
- _context5.next = 2;
1170
+ _context6.next = 2;
1163
1171
  break;
1164
1172
  }
1165
- return _context5.abrupt("return", true);
1173
+ return _context6.abrupt("return", true);
1166
1174
  case 2:
1167
- success = true;
1168
- _context5.prev = 3;
1169
- blocks = []; // Collect all reference synced blocks on the current document
1175
+ if (!fg('platform_synced_block_patch_2')) {
1176
+ _context6.next = 9;
1177
+ break;
1178
+ }
1179
+ if (!this.isFlushInProgress) {
1180
+ _context6.next = 8;
1181
+ break;
1182
+ }
1183
+ // Mark that another flush is needed after the current one completes
1184
+ this.flushNeededAfterCurrent = true;
1185
+
1186
+ // We return true here because we know the pending flush will handle the dirty cache
1187
+ return _context6.abrupt("return", true);
1188
+ case 8:
1189
+ this.isFlushInProgress = true;
1190
+ case 9:
1191
+ success = true; // a copy of the subscriptions STRUCTURE (without the callbacks)
1192
+ // To be saved as the last flushed structure if the flush is successful
1193
+ syncedBlocksToFlush = {};
1194
+ _context6.prev = 11;
1195
+ if (this.dataProvider) {
1196
+ _context6.next = 14;
1197
+ break;
1198
+ }
1199
+ throw new Error('Data provider not set');
1200
+ case 14:
1201
+ blocks = [];
1202
+ if (!fg('platform_synced_block_patch_2')) {
1203
+ _context6.next = 37;
1204
+ break;
1205
+ }
1206
+ // First, build the complete subscription structure
1207
+ _iterator4 = _createForOfIteratorHelper(this.subscriptions.entries());
1208
+ _context6.prev = 17;
1209
+ _loop2 = /*#__PURE__*/_regeneratorRuntime.mark(function _loop2() {
1210
+ var _step4$value, resourceId, callbacks;
1211
+ return _regeneratorRuntime.wrap(function _loop2$(_context5) {
1212
+ while (1) switch (_context5.prev = _context5.next) {
1213
+ case 0:
1214
+ _step4$value = _slicedToArray(_step4.value, 2), resourceId = _step4$value[0], callbacks = _step4$value[1];
1215
+ syncedBlocksToFlush[resourceId] = {};
1216
+ Object.keys(callbacks).forEach(function (localId) {
1217
+ blocks.push({
1218
+ resourceId: resourceId,
1219
+ localId: localId
1220
+ });
1221
+ syncedBlocksToFlush[resourceId][localId] = true;
1222
+ });
1223
+ case 3:
1224
+ case "end":
1225
+ return _context5.stop();
1226
+ }
1227
+ }, _loop2);
1228
+ });
1229
+ _iterator4.s();
1230
+ case 20:
1231
+ if ((_step4 = _iterator4.n()).done) {
1232
+ _context6.next = 24;
1233
+ break;
1234
+ }
1235
+ return _context6.delegateYield(_loop2(), "t0", 22);
1236
+ case 22:
1237
+ _context6.next = 20;
1238
+ break;
1239
+ case 24:
1240
+ _context6.next = 29;
1241
+ break;
1242
+ case 26:
1243
+ _context6.prev = 26;
1244
+ _context6.t1 = _context6["catch"](17);
1245
+ _iterator4.e(_context6.t1);
1246
+ case 29:
1247
+ _context6.prev = 29;
1248
+ _iterator4.f();
1249
+ return _context6.finish(29);
1250
+ case 32:
1251
+ if (!isEqual(syncedBlocksToFlush, this.lastFlushedSyncedBlocks)) {
1252
+ _context6.next = 35;
1253
+ break;
1254
+ }
1255
+ this.isCacheDirty = false; // Reset since we're considering this a successful no-op flush
1256
+ return _context6.abrupt("return", true);
1257
+ case 35:
1258
+ _context6.next = 38;
1259
+ break;
1260
+ case 37:
1261
+ // Collect all reference synced blocks on the current document
1170
1262
  Array.from(this.subscriptions.entries()).forEach(function (_ref2) {
1171
1263
  var _ref3 = _slicedToArray(_ref2, 2),
1172
1264
  resourceId = _ref3[0],
@@ -1178,12 +1270,7 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
1178
1270
  });
1179
1271
  });
1180
1272
  });
1181
- if (this.dataProvider) {
1182
- _context5.next = 8;
1183
- break;
1184
- }
1185
- throw new Error('Data provider not set');
1186
- case 8:
1273
+ case 38:
1187
1274
  // reset isCacheDirty early to prevent race condition
1188
1275
  // There is a race condition where if a user makes changes (create/delete) to a reference sync block
1189
1276
  // on a live page and the reference sync block is being saved while the user
@@ -1191,10 +1278,10 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
1191
1278
  // exactly at a time when the updateReferenceData is being executed asynchronously.
1192
1279
  this.isCacheDirty = false;
1193
1280
  (_this$saveExperience = this.saveExperience) === null || _this$saveExperience === void 0 || _this$saveExperience.start();
1194
- _context5.next = 12;
1281
+ _context6.next = 42;
1195
1282
  return this.dataProvider.updateReferenceData(blocks);
1196
- case 12:
1197
- updateResult = _context5.sent;
1283
+ case 42:
1284
+ updateResult = _context6.sent;
1198
1285
  if (!updateResult.success) {
1199
1286
  success = false;
1200
1287
  (_this$saveExperience2 = this.saveExperience) === null || _this$saveExperience2 === void 0 || _this$saveExperience2.failure({
@@ -1202,35 +1289,53 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
1202
1289
  });
1203
1290
  (_this$fireAnalyticsEv1 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv1 === void 0 || _this$fireAnalyticsEv1.call(this, updateReferenceErrorPayload(updateResult.error || 'Failed to update reference synced blocks on the document'));
1204
1291
  }
1205
- _context5.next = 22;
1292
+ _context6.next = 52;
1206
1293
  break;
1207
- case 16:
1208
- _context5.prev = 16;
1209
- _context5.t0 = _context5["catch"](3);
1294
+ case 46:
1295
+ _context6.prev = 46;
1296
+ _context6.t2 = _context6["catch"](11);
1210
1297
  success = false;
1211
- logException(_context5.t0, {
1298
+ logException(_context6.t2, {
1212
1299
  location: 'editor-synced-block-provider/referenceSyncBlockStoreManager'
1213
1300
  });
1214
1301
  (_this$saveExperience3 = this.saveExperience) === null || _this$saveExperience3 === void 0 || _this$saveExperience3.failure({
1215
- reason: _context5.t0.message
1302
+ reason: _context6.t2.message
1216
1303
  });
1217
- (_this$fireAnalyticsEv10 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv10 === void 0 || _this$fireAnalyticsEv10.call(this, updateReferenceErrorPayload(_context5.t0.message));
1218
- case 22:
1219
- _context5.prev = 22;
1304
+ (_this$fireAnalyticsEv10 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv10 === void 0 || _this$fireAnalyticsEv10.call(this, updateReferenceErrorPayload(_context6.t2.message));
1305
+ case 52:
1306
+ _context6.prev = 52;
1220
1307
  if (!success) {
1221
1308
  // set isCacheDirty back to true for cases where it failed to update the reference synced blocks on the BE
1222
1309
  this.isCacheDirty = true;
1223
1310
  } else {
1311
+ if (fg('platform_synced_block_patch_2')) {
1312
+ this.lastFlushedSyncedBlocks = syncedBlocksToFlush;
1313
+ }
1224
1314
  (_this$saveExperience4 = this.saveExperience) === null || _this$saveExperience4 === void 0 || _this$saveExperience4.success();
1225
1315
  }
1226
- return _context5.finish(22);
1227
- case 25:
1228
- return _context5.abrupt("return", success);
1229
- case 26:
1316
+ if (fg('platform_synced_block_patch_2')) {
1317
+ // Always reset isFlushInProgress regardless of feature flag
1318
+ this.isFlushInProgress = false;
1319
+
1320
+ // If another flush was requested while this one was in progress, execute it now
1321
+ if (this.flushNeededAfterCurrent) {
1322
+ this.flushNeededAfterCurrent = false;
1323
+ // Use setTimeout to avoid deep recursion and run queued flush asynchronously
1324
+ // Note: flush() handles all exceptions internally and never rejects
1325
+ this.queuedFlushTimeout = setTimeout(function () {
1326
+ _this10.queuedFlushTimeout = undefined;
1327
+ void _this10.flush();
1328
+ }, 0);
1329
+ }
1330
+ }
1331
+ return _context6.finish(52);
1332
+ case 56:
1333
+ return _context6.abrupt("return", success);
1334
+ case 57:
1230
1335
  case "end":
1231
- return _context5.stop();
1336
+ return _context6.stop();
1232
1337
  }
1233
- }, _callee4, this, [[3, 16, 22, 25]]);
1338
+ }, _callee4, this, [[11, 46, 52, 56], [17, 26, 29, 32]]);
1234
1339
  }));
1235
1340
  function flush() {
1236
1341
  return _flush.apply(this, arguments);
@@ -1241,6 +1346,12 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
1241
1346
  key: "destroy",
1242
1347
  value: function destroy() {
1243
1348
  var _this$saveExperience5, _this$fetchExperience0, _this$fetchSourceInfo2;
1349
+ // Cancel any queued flush to prevent it from running after destroy
1350
+ if (this.queuedFlushTimeout) {
1351
+ clearTimeout(this.queuedFlushTimeout);
1352
+ this.queuedFlushTimeout = undefined;
1353
+ }
1354
+
1244
1355
  // Clean up all GraphQL subscriptions first
1245
1356
  this.cleanupAllGraphQLSubscriptions();
1246
1357
  if (fg('platform_synced_block_patch_1')) {
@@ -21,7 +21,11 @@ export declare class ReferenceSyncBlockStoreManager {
21
21
  private useRealTimeSubscriptions;
22
22
  private subscriptionChangeListeners;
23
23
  private newlyAddedSyncBlocks;
24
+ private lastFlushedSyncedBlocks;
24
25
  private onUnpublishedSyncBlockDetected?;
26
+ private isFlushInProgress;
27
+ private flushNeededAfterCurrent;
28
+ private queuedFlushTimeout?;
25
29
  fetchExperience: Experience | undefined;
26
30
  private fetchSourceInfoExperience;
27
31
  private saveExperience;
@@ -21,7 +21,11 @@ export declare class ReferenceSyncBlockStoreManager {
21
21
  private useRealTimeSubscriptions;
22
22
  private subscriptionChangeListeners;
23
23
  private newlyAddedSyncBlocks;
24
+ private lastFlushedSyncedBlocks;
24
25
  private onUnpublishedSyncBlockDetected?;
26
+ private isFlushInProgress;
27
+ private flushNeededAfterCurrent;
28
+ private queuedFlushTimeout?;
25
29
  fetchExperience: Experience | undefined;
26
30
  private fetchSourceInfoExperience;
27
31
  private saveExperience;
package/package.json CHANGED
@@ -32,6 +32,7 @@
32
32
  "@babel/runtime": "^7.0.0",
33
33
  "@compiled/react": "^0.18.6",
34
34
  "graphql-ws": "^5.14.2",
35
+ "lodash": "^4.17.21",
35
36
  "raf-schd": "^4.0.3",
36
37
  "uuid": "^3.1.0"
37
38
  },
@@ -79,7 +80,7 @@
79
80
  }
80
81
  },
81
82
  "name": "@atlaskit/editor-synced-block-provider",
82
- "version": "3.27.2",
83
+ "version": "3.28.0",
83
84
  "description": "Synced Block Provider for @atlaskit/editor-plugin-synced-block",
84
85
  "author": "Atlassian Pty Ltd",
85
86
  "license": "Apache-2.0",