@atlaskit/editor-synced-block-provider 3.27.1 → 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,20 @@
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
+
10
+ ## 3.27.2
11
+
12
+ ### Patch Changes
13
+
14
+ - [`7b605d0a82c41`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/7b605d0a82c41) -
15
+ Add some tests + remove pending fetch requests for resourceId if reference blocks deleted
16
+ - Updated dependencies
17
+
3
18
  ## 3.27.1
4
19
 
5
20
  ### 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) {
@@ -833,8 +840,18 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
833
840
  }, {
834
841
  key: "debouncedBatchedFetchSyncBlocks",
835
842
  value: function debouncedBatchedFetchSyncBlocks(resourceId) {
836
- this.pendingFetchRequests.add(resourceId);
837
- this.scheduledBatchFetch();
843
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_2')) {
844
+ // Only add to pending requests if there are active subscriptions for this resource
845
+ if (this.subscriptions.has(resourceId) && Object.keys(this.subscriptions.get(resourceId) || {}).length > 0) {
846
+ this.pendingFetchRequests.add(resourceId);
847
+ this.scheduledBatchFetch();
848
+ } else {
849
+ this.pendingFetchRequests.delete(resourceId);
850
+ }
851
+ } else {
852
+ this.pendingFetchRequests.add(resourceId);
853
+ this.scheduledBatchFetch();
854
+ }
838
855
  }
839
856
  }, {
840
857
  key: "subscribeToSyncBlock",
@@ -1150,19 +1167,104 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
1150
1167
  key: "flush",
1151
1168
  value: (function () {
1152
1169
  var _flush = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4() {
1153
- var success, _this$saveExperience, blocks, updateResult, _this$saveExperience2, _this$fireAnalyticsEv1, _this$saveExperience3, _this$fireAnalyticsEv10, _this$saveExperience4;
1154
- return _regenerator.default.wrap(function _callee4$(_context5) {
1155
- 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) {
1156
1174
  case 0:
1157
1175
  if (this.isCacheDirty) {
1158
- _context5.next = 2;
1176
+ _context6.next = 2;
1159
1177
  break;
1160
1178
  }
1161
- return _context5.abrupt("return", true);
1179
+ return _context6.abrupt("return", true);
1162
1180
  case 2:
1163
- success = true;
1164
- _context5.prev = 3;
1165
- 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
1166
1268
  Array.from(this.subscriptions.entries()).forEach(function (_ref2) {
1167
1269
  var _ref3 = (0, _slicedToArray2.default)(_ref2, 2),
1168
1270
  resourceId = _ref3[0],
@@ -1174,12 +1276,7 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
1174
1276
  });
1175
1277
  });
1176
1278
  });
1177
- if (this.dataProvider) {
1178
- _context5.next = 8;
1179
- break;
1180
- }
1181
- throw new Error('Data provider not set');
1182
- case 8:
1279
+ case 38:
1183
1280
  // reset isCacheDirty early to prevent race condition
1184
1281
  // There is a race condition where if a user makes changes (create/delete) to a reference sync block
1185
1282
  // on a live page and the reference sync block is being saved while the user
@@ -1187,10 +1284,10 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
1187
1284
  // exactly at a time when the updateReferenceData is being executed asynchronously.
1188
1285
  this.isCacheDirty = false;
1189
1286
  (_this$saveExperience = this.saveExperience) === null || _this$saveExperience === void 0 || _this$saveExperience.start();
1190
- _context5.next = 12;
1287
+ _context6.next = 42;
1191
1288
  return this.dataProvider.updateReferenceData(blocks);
1192
- case 12:
1193
- updateResult = _context5.sent;
1289
+ case 42:
1290
+ updateResult = _context6.sent;
1194
1291
  if (!updateResult.success) {
1195
1292
  success = false;
1196
1293
  (_this$saveExperience2 = this.saveExperience) === null || _this$saveExperience2 === void 0 || _this$saveExperience2.failure({
@@ -1198,35 +1295,53 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
1198
1295
  });
1199
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'));
1200
1297
  }
1201
- _context5.next = 22;
1298
+ _context6.next = 52;
1202
1299
  break;
1203
- case 16:
1204
- _context5.prev = 16;
1205
- _context5.t0 = _context5["catch"](3);
1300
+ case 46:
1301
+ _context6.prev = 46;
1302
+ _context6.t2 = _context6["catch"](11);
1206
1303
  success = false;
1207
- (0, _monitoring.logException)(_context5.t0, {
1304
+ (0, _monitoring.logException)(_context6.t2, {
1208
1305
  location: 'editor-synced-block-provider/referenceSyncBlockStoreManager'
1209
1306
  });
1210
1307
  (_this$saveExperience3 = this.saveExperience) === null || _this$saveExperience3 === void 0 || _this$saveExperience3.failure({
1211
- reason: _context5.t0.message
1308
+ reason: _context6.t2.message
1212
1309
  });
1213
- (_this$fireAnalyticsEv10 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv10 === void 0 || _this$fireAnalyticsEv10.call(this, (0, _errorHandling.updateReferenceErrorPayload)(_context5.t0.message));
1214
- case 22:
1215
- _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;
1216
1313
  if (!success) {
1217
1314
  // set isCacheDirty back to true for cases where it failed to update the reference synced blocks on the BE
1218
1315
  this.isCacheDirty = true;
1219
1316
  } else {
1317
+ if ((0, _platformFeatureFlags.fg)('platform_synced_block_patch_2')) {
1318
+ this.lastFlushedSyncedBlocks = syncedBlocksToFlush;
1319
+ }
1220
1320
  (_this$saveExperience4 = this.saveExperience) === null || _this$saveExperience4 === void 0 || _this$saveExperience4.success();
1221
1321
  }
1222
- return _context5.finish(22);
1223
- case 25:
1224
- return _context5.abrupt("return", success);
1225
- 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:
1226
1341
  case "end":
1227
- return _context5.stop();
1342
+ return _context6.stop();
1228
1343
  }
1229
- }, _callee4, this, [[3, 16, 22, 25]]);
1344
+ }, _callee4, this, [[11, 46, 52, 56], [17, 26, 29, 32]]);
1230
1345
  }));
1231
1346
  function flush() {
1232
1347
  return _flush.apply(this, arguments);
@@ -1237,6 +1352,12 @@ var ReferenceSyncBlockStoreManager = exports.ReferenceSyncBlockStoreManager = /*
1237
1352
  key: "destroy",
1238
1353
  value: function destroy() {
1239
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
+
1240
1361
  // Clean up all GraphQL subscriptions first
1241
1362
  this.cleanupAllGraphQLSubscriptions();
1242
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) {
@@ -639,8 +646,18 @@ export class ReferenceSyncBlockStoreManager {
639
646
  this.providerFactories.delete(resourceId);
640
647
  }
641
648
  debouncedBatchedFetchSyncBlocks(resourceId) {
642
- this.pendingFetchRequests.add(resourceId);
643
- this.scheduledBatchFetch();
649
+ if (fg('platform_synced_block_patch_2')) {
650
+ // Only add to pending requests if there are active subscriptions for this resource
651
+ if (this.subscriptions.has(resourceId) && Object.keys(this.subscriptions.get(resourceId) || {}).length > 0) {
652
+ this.pendingFetchRequests.add(resourceId);
653
+ this.scheduledBatchFetch();
654
+ } else {
655
+ this.pendingFetchRequests.delete(resourceId);
656
+ }
657
+ } else {
658
+ this.pendingFetchRequests.add(resourceId);
659
+ this.scheduledBatchFetch();
660
+ }
644
661
  }
645
662
  subscribeToSyncBlock(resourceId, localId, callback) {
646
663
  var _this$dataProvider7, _this$dataProvider7$g, _this$dataProvider8, _this$dataProvider8$g, _this$dataProvider9, _this$dataProvider9$g;
@@ -950,24 +967,63 @@ export class ReferenceSyncBlockStoreManager {
950
967
  */
951
968
  async flush() {
952
969
  if (!this.isCacheDirty) {
970
+ // we use the isCacheDirty flag as a quick check.
953
971
  return true;
954
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
+ }
955
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 = {};
956
990
  try {
957
991
  var _this$saveExperience;
992
+ if (!this.dataProvider) {
993
+ throw new Error('Data provider not set');
994
+ }
958
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
+ }
959
1008
 
960
- // Collect all reference synced blocks on the current document
961
- Array.from(this.subscriptions.entries()).forEach(([resourceId, callbacks]) => {
962
- Object.keys(callbacks).forEach(localId => {
963
- blocks.push({
964
- resourceId,
965
- 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
+ });
966
1025
  });
967
1026
  });
968
- });
969
- if (!this.dataProvider) {
970
- throw new Error('Data provider not set');
971
1027
  }
972
1028
 
973
1029
  // reset isCacheDirty early to prevent race condition
@@ -1002,13 +1058,37 @@ export class ReferenceSyncBlockStoreManager {
1002
1058
  this.isCacheDirty = true;
1003
1059
  } else {
1004
1060
  var _this$saveExperience4;
1061
+ if (fg('platform_synced_block_patch_2')) {
1062
+ this.lastFlushedSyncedBlocks = syncedBlocksToFlush;
1063
+ }
1005
1064
  (_this$saveExperience4 = this.saveExperience) === null || _this$saveExperience4 === void 0 ? void 0 : _this$saveExperience4.success();
1006
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
+ }
1007
1081
  }
1008
1082
  return success;
1009
1083
  }
1010
1084
  destroy() {
1011
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
+
1012
1092
  // Clean up all GraphQL subscriptions first
1013
1093
  this.cleanupAllGraphQLSubscriptions();
1014
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) {
@@ -827,8 +834,18 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
827
834
  }, {
828
835
  key: "debouncedBatchedFetchSyncBlocks",
829
836
  value: function debouncedBatchedFetchSyncBlocks(resourceId) {
830
- this.pendingFetchRequests.add(resourceId);
831
- this.scheduledBatchFetch();
837
+ if (fg('platform_synced_block_patch_2')) {
838
+ // Only add to pending requests if there are active subscriptions for this resource
839
+ if (this.subscriptions.has(resourceId) && Object.keys(this.subscriptions.get(resourceId) || {}).length > 0) {
840
+ this.pendingFetchRequests.add(resourceId);
841
+ this.scheduledBatchFetch();
842
+ } else {
843
+ this.pendingFetchRequests.delete(resourceId);
844
+ }
845
+ } else {
846
+ this.pendingFetchRequests.add(resourceId);
847
+ this.scheduledBatchFetch();
848
+ }
832
849
  }
833
850
  }, {
834
851
  key: "subscribeToSyncBlock",
@@ -1144,19 +1161,104 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
1144
1161
  key: "flush",
1145
1162
  value: (function () {
1146
1163
  var _flush = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee4() {
1147
- var success, _this$saveExperience, blocks, updateResult, _this$saveExperience2, _this$fireAnalyticsEv1, _this$saveExperience3, _this$fireAnalyticsEv10, _this$saveExperience4;
1148
- return _regeneratorRuntime.wrap(function _callee4$(_context5) {
1149
- 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) {
1150
1168
  case 0:
1151
1169
  if (this.isCacheDirty) {
1152
- _context5.next = 2;
1170
+ _context6.next = 2;
1153
1171
  break;
1154
1172
  }
1155
- return _context5.abrupt("return", true);
1173
+ return _context6.abrupt("return", true);
1156
1174
  case 2:
1157
- success = true;
1158
- _context5.prev = 3;
1159
- 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
1160
1262
  Array.from(this.subscriptions.entries()).forEach(function (_ref2) {
1161
1263
  var _ref3 = _slicedToArray(_ref2, 2),
1162
1264
  resourceId = _ref3[0],
@@ -1168,12 +1270,7 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
1168
1270
  });
1169
1271
  });
1170
1272
  });
1171
- if (this.dataProvider) {
1172
- _context5.next = 8;
1173
- break;
1174
- }
1175
- throw new Error('Data provider not set');
1176
- case 8:
1273
+ case 38:
1177
1274
  // reset isCacheDirty early to prevent race condition
1178
1275
  // There is a race condition where if a user makes changes (create/delete) to a reference sync block
1179
1276
  // on a live page and the reference sync block is being saved while the user
@@ -1181,10 +1278,10 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
1181
1278
  // exactly at a time when the updateReferenceData is being executed asynchronously.
1182
1279
  this.isCacheDirty = false;
1183
1280
  (_this$saveExperience = this.saveExperience) === null || _this$saveExperience === void 0 || _this$saveExperience.start();
1184
- _context5.next = 12;
1281
+ _context6.next = 42;
1185
1282
  return this.dataProvider.updateReferenceData(blocks);
1186
- case 12:
1187
- updateResult = _context5.sent;
1283
+ case 42:
1284
+ updateResult = _context6.sent;
1188
1285
  if (!updateResult.success) {
1189
1286
  success = false;
1190
1287
  (_this$saveExperience2 = this.saveExperience) === null || _this$saveExperience2 === void 0 || _this$saveExperience2.failure({
@@ -1192,35 +1289,53 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
1192
1289
  });
1193
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'));
1194
1291
  }
1195
- _context5.next = 22;
1292
+ _context6.next = 52;
1196
1293
  break;
1197
- case 16:
1198
- _context5.prev = 16;
1199
- _context5.t0 = _context5["catch"](3);
1294
+ case 46:
1295
+ _context6.prev = 46;
1296
+ _context6.t2 = _context6["catch"](11);
1200
1297
  success = false;
1201
- logException(_context5.t0, {
1298
+ logException(_context6.t2, {
1202
1299
  location: 'editor-synced-block-provider/referenceSyncBlockStoreManager'
1203
1300
  });
1204
1301
  (_this$saveExperience3 = this.saveExperience) === null || _this$saveExperience3 === void 0 || _this$saveExperience3.failure({
1205
- reason: _context5.t0.message
1302
+ reason: _context6.t2.message
1206
1303
  });
1207
- (_this$fireAnalyticsEv10 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv10 === void 0 || _this$fireAnalyticsEv10.call(this, updateReferenceErrorPayload(_context5.t0.message));
1208
- case 22:
1209
- _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;
1210
1307
  if (!success) {
1211
1308
  // set isCacheDirty back to true for cases where it failed to update the reference synced blocks on the BE
1212
1309
  this.isCacheDirty = true;
1213
1310
  } else {
1311
+ if (fg('platform_synced_block_patch_2')) {
1312
+ this.lastFlushedSyncedBlocks = syncedBlocksToFlush;
1313
+ }
1214
1314
  (_this$saveExperience4 = this.saveExperience) === null || _this$saveExperience4 === void 0 || _this$saveExperience4.success();
1215
1315
  }
1216
- return _context5.finish(22);
1217
- case 25:
1218
- return _context5.abrupt("return", success);
1219
- 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:
1220
1335
  case "end":
1221
- return _context5.stop();
1336
+ return _context6.stop();
1222
1337
  }
1223
- }, _callee4, this, [[3, 16, 22, 25]]);
1338
+ }, _callee4, this, [[11, 46, 52, 56], [17, 26, 29, 32]]);
1224
1339
  }));
1225
1340
  function flush() {
1226
1341
  return _flush.apply(this, arguments);
@@ -1231,6 +1346,12 @@ export var ReferenceSyncBlockStoreManager = /*#__PURE__*/function () {
1231
1346
  key: "destroy",
1232
1347
  value: function destroy() {
1233
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
+
1234
1355
  // Clean up all GraphQL subscriptions first
1235
1356
  this.cleanupAllGraphQLSubscriptions();
1236
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.1",
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",