@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.cjs CHANGED
@@ -2009,7 +2009,9 @@ const createDecorations = (
2009
2009
  return
2010
2010
  }
2011
2011
 
2012
- if (aw.cursor != null) {
2012
+ // `aw` can be null when a client disconnects, so we guard against it
2013
+ // before reading `cursor` to avoid a TypeError.
2014
+ if (aw && aw.cursor != null) {
2013
2015
  const user = aw.user || {};
2014
2016
  if (user.color == null) {
2015
2017
  user.color = '#ffa500';
@@ -2124,6 +2126,9 @@ const yCursorPlugin = (
2124
2126
  };
2125
2127
  const updateCursorInfo = () => {
2126
2128
  const ystate = ySyncPluginKey.getState(view.state);
2129
+ // `ystate` is undefined during sync-plugin init and Y.Doc transitions;
2130
+ // reading from it here would throw and break cursor tracking for the view.
2131
+ if (!ystate || !ystate.binding) return
2127
2132
  // @note We make implicit checks when checking for the cursor property
2128
2133
  const current = awareness.getLocalState() || {};
2129
2134
  if (view.hasFocus()) {
@@ -2218,11 +2223,26 @@ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins =
2218
2223
  init: (initargs, state) => {
2219
2224
  // TODO: check if plugin order matches and fix
2220
2225
  const ystate = ySyncPluginKey.getState(state);
2221
- const _undoManager = undoManager || new Y.UndoManager(ystate.type, {
2222
- trackedOrigins: new Set([ySyncPluginKey].concat(trackedOrigins)),
2223
- deleteFilter: (item) => defaultDeleteFilter(item, protectedNodes),
2224
- captureTransaction: tr => tr.meta.get('addToHistory') !== false
2225
- });
2226
+ let _undoManager = undoManager;
2227
+ if (!_undoManager) {
2228
+ // Y.UndoManager registers a `doc.on('destroy', )` listener in its
2229
+ // constructor that UndoManager.destroy() never removes. When the doc
2230
+ // outlives the editor (e.g. several editors sharing one provider), that
2231
+ // listener keeps the UndoManager — and everything it references —
2232
+ // reachable from the doc, leaking memory on every editor destroy.
2233
+ // We only own the lifecycle of a manager we create here, so capture the
2234
+ // listener(s) it adds and remove them when the plugin view is destroyed.
2235
+ const doc = ystate.doc;
2236
+ const destroyListenersBefore = new Set(doc ? doc._observers.get('destroy') : []);
2237
+ _undoManager = new Y.UndoManager(ystate.type, {
2238
+ trackedOrigins: new Set([ySyncPluginKey].concat(trackedOrigins)),
2239
+ deleteFilter: (item) => defaultDeleteFilter(item, protectedNodes),
2240
+ captureTransaction: tr => tr.meta.get('addToHistory') !== false
2241
+ });
2242
+ const destroyListenersAfter = doc ? doc._observers.get('destroy') : new Set();
2243
+ _undoManager._yTiptapDocDestroyListeners = Array.from(destroyListenersAfter || [])
2244
+ .filter(listener => !destroyListenersBefore.has(listener));
2245
+ }
2226
2246
  return {
2227
2247
  undoManager: _undoManager,
2228
2248
  prevSel: null,
@@ -2275,6 +2295,13 @@ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins =
2275
2295
  return {
2276
2296
  destroy: () => {
2277
2297
  undoManager.destroy();
2298
+ // Remove the doc 'destroy' listener Y.UndoManager fails to clean up
2299
+ // (only for managers we created — see state.init above).
2300
+ const leakedDestroyListeners = undoManager._yTiptapDocDestroyListeners;
2301
+ if (leakedDestroyListeners && undoManager.doc) {
2302
+ leakedDestroyListeners.forEach(listener => undoManager.doc.off('destroy', listener));
2303
+ undoManager._yTiptapDocDestroyListeners = null;
2304
+ }
2278
2305
  }
2279
2306
  }
2280
2307
  }