@atlaskit/editor-synced-block-provider 4.1.0 → 4.1.2

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.
@@ -1,7 +1,6 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import isEqual from 'lodash/isEqual';
3
3
  import { logException } from '@atlaskit/editor-common/monitoring';
4
- import { fg } from '@atlaskit/platform-feature-flags';
5
4
  import { SyncBlockError } from '../common/types';
6
5
  import { updateErrorPayload, createErrorPayload, deleteErrorPayload, updateCacheErrorPayload, getSourceInfoErrorPayload, updateSuccessPayload, createSuccessPayload, deleteSuccessPayload, fetchReferencesErrorPayload } from '../utils/errorHandling';
7
6
  import { getCreateSourceExperience, getDeleteSourceExperience, getSaveSourceExperience, getFetchSourceInfoExperience } from '../utils/experienceTracking';
@@ -52,11 +51,9 @@ export class SourceSyncBlockStoreManager {
52
51
  throw new Error('Local ID or resource ID is not set');
53
52
  }
54
53
  const syncBlockData = convertSyncBlockPMNodeToSyncBlockData(syncBlockNode);
55
- if (fg('platform_synced_block_patch_5')) {
56
- const cachedBlock = this.syncBlockCache.get(resourceId);
57
- if (cachedBlock && !isEqual(syncBlockData.content, cachedBlock.content)) {
58
- this.hasReceivedContentChange = true;
59
- }
54
+ const cachedBlock = this.syncBlockCache.get(resourceId);
55
+ if (cachedBlock && !isEqual(syncBlockData.content, cachedBlock.content)) {
56
+ this.hasReceivedContentChange = true;
60
57
  }
61
58
  this.syncBlockCache.set(resourceId, {
62
59
  ...syncBlockData,
@@ -1,20 +1,35 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import { logException } from '@atlaskit/editor-common/monitoring';
3
- import { fg } from '@atlaskit/platform-feature-flags';
4
3
  import { fetchErrorPayload, fetchSuccessPayload } from '../utils/errorHandling';
5
4
  import { resolveSyncBlockInstance } from '../utils/resolveSyncBlockInstance';
6
5
  /**
7
6
  * Manages the lifecycle of GraphQL WebSocket subscriptions for sync block
8
- * real-time updates, and provides a listener API so React components can
9
- * react when the set of subscribed resource IDs changes.
7
+ * real-time updates, owns the subscriptions and titleSubscriptions maps,
8
+ * and provides a listener API so React components can react when the set
9
+ * of subscribed resource IDs changes.
10
10
  */
11
11
  export class SyncBlockSubscriptionManager {
12
12
  constructor(deps) {
13
+ _defineProperty(this, "subscriptions", new Map());
14
+ _defineProperty(this, "titleSubscriptions", new Map());
13
15
  _defineProperty(this, "graphqlSubscriptions", new Map());
14
16
  _defineProperty(this, "subscriptionChangeListeners", new Set());
15
17
  _defineProperty(this, "useRealTimeSubscriptions", false);
18
+ // Track pending cache deletions to handle block moves (unmount/remount)
19
+ // When a block is moved, the old component unmounts before the new one mounts,
20
+ // causing the cache to be deleted prematurely. We delay deletion to allow
21
+ // the new component to subscribe and cancel the pending deletion.
22
+ _defineProperty(this, "pendingCacheDeletions", new Map());
16
23
  this.deps = deps;
17
24
  }
25
+
26
+ /**
27
+ * Returns the subscriptions map. Used by external consumers (e.g. batch fetcher, flush)
28
+ * that need to read the current subscription state.
29
+ */
30
+ getSubscriptions() {
31
+ return this.subscriptions;
32
+ }
18
33
  setRealTimeSubscriptionsEnabled(enabled) {
19
34
  if (this.useRealTimeSubscriptions === enabled) {
20
35
  return;
@@ -30,7 +45,7 @@ export class SyncBlockSubscriptionManager {
30
45
  return this.useRealTimeSubscriptions;
31
46
  }
32
47
  getSubscribedResourceIds() {
33
- return Array.from(this.deps.getSubscriptions().keys());
48
+ return Array.from(this.subscriptions.keys());
34
49
  }
35
50
  onSubscriptionsChanged(listener) {
36
51
  this.subscriptionChangeListeners.add(listener);
@@ -62,6 +77,133 @@ export class SyncBlockSubscriptionManager {
62
77
  this.deps.fetchSyncBlockSourceInfo(resolved.resourceId);
63
78
  }
64
79
  }
80
+ subscribeToSyncBlock(resourceId, localId, callback) {
81
+ // Cancel any pending cache deletion for this resourceId.
82
+ // This handles the case where a block is moved - the old component unmounts
83
+ // (scheduling deletion) but the new component mounts and subscribes before
84
+ // the deletion timeout fires.
85
+ const pendingDeletion = this.pendingCacheDeletions.get(resourceId);
86
+ if (pendingDeletion) {
87
+ clearTimeout(pendingDeletion);
88
+ this.pendingCacheDeletions.delete(resourceId);
89
+ }
90
+
91
+ // add to subscriptions map
92
+ const resourceSubscriptions = this.subscriptions.get(resourceId) || {};
93
+ const isNewResourceSubscription = Object.keys(resourceSubscriptions).length === 0;
94
+ this.subscriptions.set(resourceId, {
95
+ ...resourceSubscriptions,
96
+ [localId]: callback
97
+ });
98
+
99
+ // New subscription means new reference synced block is added to the document
100
+ this.deps.markCacheDirty();
101
+
102
+ // Notify listeners if this is a new resource subscription
103
+ if (isNewResourceSubscription) {
104
+ this.notifySubscriptionChangeListeners();
105
+ }
106
+ const cachedData = this.deps.getFromCache(resourceId);
107
+ if (cachedData) {
108
+ callback(cachedData);
109
+ } else {
110
+ this.deps.debouncedBatchedFetchSyncBlocks(resourceId);
111
+ }
112
+
113
+ // Set up GraphQL subscription if real-time subscriptions are enabled
114
+ if (this.shouldUseRealTime()) {
115
+ this.setupSubscription(resourceId);
116
+ }
117
+ return () => {
118
+ const resourceSubscriptions = this.subscriptions.get(resourceId);
119
+ if (resourceSubscriptions) {
120
+ // Unsubscription means a reference synced block is removed from the document
121
+ this.deps.markCacheDirty();
122
+ delete resourceSubscriptions[localId];
123
+ if (Object.keys(resourceSubscriptions).length === 0) {
124
+ this.subscriptions.delete(resourceId);
125
+
126
+ // Clean up GraphQL subscription when no more local subscribers
127
+ this.cleanupSubscription(resourceId);
128
+
129
+ // Notify listeners that subscription was removed
130
+ this.notifySubscriptionChangeListeners();
131
+
132
+ // Delay cache deletion to handle block moves (unmount/remount).
133
+ // When a block is moved, the old component unmounts before the new one mounts.
134
+ // By delaying deletion, we give the new component time to subscribe and
135
+ // cancel this pending deletion, preserving the cached data.
136
+ // TODO: EDITOR-4152 - Rework this logic
137
+ const deletionTimeout = setTimeout(() => {
138
+ // Only delete if still no subscribers (wasn't re-subscribed)
139
+ if (!this.subscriptions.has(resourceId)) {
140
+ this.deps.deleteFromCache(resourceId);
141
+ }
142
+ this.pendingCacheDeletions.delete(resourceId);
143
+ }, 1000);
144
+ this.pendingCacheDeletions.set(resourceId, deletionTimeout);
145
+ } else {
146
+ this.subscriptions.set(resourceId, resourceSubscriptions);
147
+ }
148
+ }
149
+ };
150
+ }
151
+ subscribeToSourceTitle(node, callback) {
152
+ var _cachedData$data;
153
+ // check node is a sync block, as we only support sync block subscriptions
154
+ if (node.type.name !== 'syncBlock') {
155
+ return () => {};
156
+ }
157
+ const {
158
+ resourceId,
159
+ localId
160
+ } = node.attrs;
161
+ if (!localId || !resourceId) {
162
+ return () => {};
163
+ }
164
+ const cachedData = this.deps.getFromCache(resourceId);
165
+ if (cachedData !== null && cachedData !== void 0 && (_cachedData$data = cachedData.data) !== null && _cachedData$data !== void 0 && _cachedData$data.sourceTitle) {
166
+ callback(cachedData.data.sourceTitle);
167
+ }
168
+
169
+ // add to subscriptions map
170
+ const resourceSubscriptions = this.titleSubscriptions.get(resourceId) || {};
171
+ this.titleSubscriptions.set(resourceId, {
172
+ ...resourceSubscriptions,
173
+ [localId]: callback
174
+ });
175
+ return () => {
176
+ const resourceSubscriptions = this.titleSubscriptions.get(resourceId);
177
+ if (resourceSubscriptions) {
178
+ delete resourceSubscriptions[localId];
179
+ if (Object.keys(resourceSubscriptions).length === 0) {
180
+ this.titleSubscriptions.delete(resourceId);
181
+ } else {
182
+ this.titleSubscriptions.set(resourceId, resourceSubscriptions);
183
+ }
184
+ }
185
+ };
186
+ }
187
+ updateSourceTitleSubscriptions(resourceId, title) {
188
+ const callbacks = this.titleSubscriptions.get(resourceId);
189
+ if (callbacks) {
190
+ Object.values(callbacks).forEach(callback => {
191
+ callback(title);
192
+ });
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Notifies all subscription callbacks for a given resource ID with the provided sync block instance.
198
+ */
199
+ notifySubscriptionCallbacks(resourceId, syncBlock) {
200
+ const callbacks = this.subscriptions.get(resourceId);
201
+ if (callbacks) {
202
+ Object.values(callbacks).forEach(callback => {
203
+ callback(syncBlock);
204
+ });
205
+ }
206
+ }
65
207
  setupSubscription(resourceId) {
66
208
  if (this.graphqlSubscriptions.has(resourceId)) {
67
209
  return;
@@ -91,7 +233,7 @@ export class SyncBlockSubscriptionManager {
91
233
  }
92
234
  }
93
235
  setupSubscriptionsForAllBlocks() {
94
- for (const resourceId of this.deps.getSubscriptions().keys()) {
236
+ for (const resourceId of this.subscriptions.keys()) {
95
237
  this.setupSubscription(resourceId);
96
238
  }
97
239
  }
@@ -103,24 +245,29 @@ export class SyncBlockSubscriptionManager {
103
245
  }
104
246
  destroy() {
105
247
  this.cleanupAll();
248
+ this.subscriptions.clear();
249
+ this.titleSubscriptions.clear();
106
250
  this.subscriptionChangeListeners.clear();
107
251
  this.useRealTimeSubscriptions = false;
252
+
253
+ // Clear any pending cache deletions
254
+ for (const timeout of this.pendingCacheDeletions.values()) {
255
+ clearTimeout(timeout);
256
+ }
257
+ this.pendingCacheDeletions.clear();
108
258
  }
109
259
  shouldUseRealTime() {
110
260
  return this.useRealTimeSubscriptions;
111
261
  }
112
262
  handleGraphQLUpdate(syncBlockInstance) {
113
263
  if (!syncBlockInstance.resourceId) {
114
- if (fg('platform_synced_block_patch_5')) {
115
- return;
116
- }
117
- throw new Error('Sync block instance provided to graphql subscription update missing resource id');
264
+ return;
118
265
  }
119
266
  const existing = this.deps.getFromCache(syncBlockInstance.resourceId);
120
267
  const resolved = existing ? resolveSyncBlockInstance(existing, syncBlockInstance) : syncBlockInstance;
121
268
  this.deps.updateCache(resolved);
122
269
  if (!syncBlockInstance.error) {
123
- const callbacks = this.deps.getSubscriptions().get(syncBlockInstance.resourceId);
270
+ const callbacks = this.subscriptions.get(syncBlockInstance.resourceId);
124
271
  const localIds = callbacks ? Object.keys(callbacks) : [];
125
272
  localIds.forEach(localId => {
126
273
  var _this$deps$getFireAna3, _syncBlockInstance$da;