{"record":{"id":"54e90bf8c70008cc","repo":"siyuan-note/siyuan","slug":"cannot-track-a-range-in-a-destroyed-protyle-instan","errorCode":null,"errorMessage":"Cannot track a range in a destroyed Protyle instance","messagePattern":"Cannot track a range in a destroyed Protyle instance","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/protyle/util/trackedRange.ts","lineNumber":1134,"sourceCode":"        removeInputListeners(protyle);\n    }\n    if (state.owner) {\n        const handles = ownedHandles.get(state.owner);\n        handles?.forEach(item => {\n            if (item.protyle === protyle && item.handle === handle) {\n                handles.delete(item);\n            }\n        });\n        if (handles?.size === 0) {\n            ownedHandles.delete(state.owner);\n        }\n    }\n};\n\nexport const trackRange = (protyle: IProtyle, range: Range,\n                           options: ITrackRangeOptions): ITrackedRangeHandle => {\n    if (destroyedProtyles.has(protyle)) {\n        throw new Error(\"Cannot track a range in a destroyed Protyle instance\");\n    }\n    if (!options?.owner || ![\"function\", \"object\"].includes(typeof options.owner)) {\n        throw new TypeError(\"The tracked range owner is required\");\n    }\n    if (options.affinity && ![\"after\", \"before\"].includes(options.affinity)) {\n        throw new TypeError(\"The tracked range affinity must be before or after\");\n    }\n    if (unloadingPlugins.has(options.owner)) {\n        throw new Error(\"Cannot track a range for an unloaded plugin\");\n    }\n    const rangeSnapshot = getRangeSnapshot(protyle, range);\n    if (!rangeSnapshot) {\n        throw new TypeError(\"The range must be inside one editable source block of this Protyle instance\");\n    }\n    const targetTokens = rangeSnapshot.stream.tokens.slice(rangeSnapshot.start, rangeSnapshot.end);\n    if (!range.collapsed && targetTokens.length === 0) {\n        throw new TypeError(\"The tracked range must contain semantic content\");\n    }","sourceCodeStart":1116,"sourceCodeEnd":1152,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/app/src/protyle/util/trackedRange.ts#L1116-L1152","documentation":"Thrown by trackRange when the passed IProtyle instance has been destroyed (it is in the destroyedProtyles set). Range tracking relies on live editor internals, so tracking in a destroyed editor is refused up front. Callers must only track ranges on active Protyle instances.","triggerScenarios":"Calling trackRange(protyle, range, options) after the protyle was destroyed (e.g. after protyle.destroy() or a tab/editor close) — typically from a stale callback or a plugin holding a stale protyle reference.","commonSituations":"Plugins caching protyle across tab switches; asynchronous callbacks (fetch, timers) resolving after the editor was destroyed; event listeners not unregistered on destroy.","solutions":["Verify the protyle is still active before calling trackRange (remove it from your tracking on destroy)","Clear listeners/timers/callbacks holding the protyle when it is destroyed","Catch the error and skip range tracking for the dead instance","Re-acquire the current protyle from the layout (e.g. the active editor) instead of a cached reference"],"exampleFix":"// before\ntrackRange(cachedProtyle, range, {owner: plugin});\n// after\nif (isProtyleDestroyed(cachedProtyle)) return;\ntrackRange(cachedProtyle, range, {owner: plugin});","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  trackRange(protyle, range, {owner: plugin});\n} catch (e) {\n  if (e.message.startsWith(\"Cannot track a range in a destroyed\")) return; // editor already closed\n  throw e;\n}","preventionTips":["Unhook all async callbacks and listeners when a Protyle is destroyed","Never cache protyle references across tab closes; fetch the active editor each time","Check whether the editor is still open before range operations in deferred code","Centralize editor access so destroy can proactively cancel pending range work"],"tags":["protyle","lifecycle","editor"],"backgroundTag":"invalid-state-transition","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}