@tiptap/y-tiptap 3.0.3 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/y-tiptap.js CHANGED
@@ -1975,7 +1975,9 @@ const createDecorations = (
1975
1975
  return
1976
1976
  }
1977
1977
 
1978
- if (aw.cursor != null) {
1978
+ // `aw` can be null when a client disconnects, so we guard against it
1979
+ // before reading `cursor` to avoid a TypeError.
1980
+ if (aw && aw.cursor != null) {
1979
1981
  const user = aw.user || {};
1980
1982
  if (user.color == null) {
1981
1983
  user.color = '#ffa500';
@@ -2090,6 +2092,9 @@ const yCursorPlugin = (
2090
2092
  };
2091
2093
  const updateCursorInfo = () => {
2092
2094
  const ystate = ySyncPluginKey.getState(view.state);
2095
+ // `ystate` is undefined during sync-plugin init and Y.Doc transitions;
2096
+ // reading from it here would throw and break cursor tracking for the view.
2097
+ if (!ystate || !ystate.binding) return
2093
2098
  // @note We make implicit checks when checking for the cursor property
2094
2099
  const current = awareness.getLocalState() || {};
2095
2100
  if (view.hasFocus()) {
@@ -2184,11 +2189,26 @@ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins =
2184
2189
  init: (initargs, state) => {
2185
2190
  // TODO: check if plugin order matches and fix
2186
2191
  const ystate = ySyncPluginKey.getState(state);
2187
- const _undoManager = undoManager || new UndoManager(ystate.type, {
2188
- trackedOrigins: new Set([ySyncPluginKey].concat(trackedOrigins)),
2189
- deleteFilter: (item) => defaultDeleteFilter(item, protectedNodes),
2190
- captureTransaction: tr => tr.meta.get('addToHistory') !== false
2191
- });
2192
+ let _undoManager = undoManager;
2193
+ if (!_undoManager) {
2194
+ // Y.UndoManager registers a `doc.on('destroy', )` listener in its
2195
+ // constructor that UndoManager.destroy() never removes. When the doc
2196
+ // outlives the editor (e.g. several editors sharing one provider), that
2197
+ // listener keeps the UndoManager — and everything it references —
2198
+ // reachable from the doc, leaking memory on every editor destroy.
2199
+ // We only own the lifecycle of a manager we create here, so capture the
2200
+ // listener(s) it adds and remove them when the plugin view is destroyed.
2201
+ const doc = ystate.doc;
2202
+ const destroyListenersBefore = new Set(doc ? doc._observers.get('destroy') : []);
2203
+ _undoManager = new UndoManager(ystate.type, {
2204
+ trackedOrigins: new Set([ySyncPluginKey].concat(trackedOrigins)),
2205
+ deleteFilter: (item) => defaultDeleteFilter(item, protectedNodes),
2206
+ captureTransaction: tr => tr.meta.get('addToHistory') !== false
2207
+ });
2208
+ const destroyListenersAfter = doc ? doc._observers.get('destroy') : new Set();
2209
+ _undoManager._yTiptapDocDestroyListeners = Array.from(destroyListenersAfter || [])
2210
+ .filter(listener => !destroyListenersBefore.has(listener));
2211
+ }
2192
2212
  return {
2193
2213
  undoManager: _undoManager,
2194
2214
  prevSel: null,
@@ -2241,6 +2261,13 @@ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins =
2241
2261
  return {
2242
2262
  destroy: () => {
2243
2263
  undoManager.destroy();
2264
+ // Remove the doc 'destroy' listener Y.UndoManager fails to clean up
2265
+ // (only for managers we created — see state.init above).
2266
+ const leakedDestroyListeners = undoManager._yTiptapDocDestroyListeners;
2267
+ if (leakedDestroyListeners && undoManager.doc) {
2268
+ leakedDestroyListeners.forEach(listener => undoManager.doc.off('destroy', listener));
2269
+ undoManager._yTiptapDocDestroyListeners = null;
2270
+ }
2244
2271
  }
2245
2272
  }
2246
2273
  }