{"record":{"id":"30159026be3e14ee","repo":"moeru-ai/airi","slug":"spine-failed-to-dispose-spinecanvas","errorCode":null,"errorMessage":"[Spine] Failed to dispose SpineCanvas:","messagePattern":"\\[Spine\\] Failed to dispose SpineCanvas:","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/stage-ui-spine/src/components/scenes/spine/Model.vue","lineNumber":101,"sourceCode":"// Mutable defaults handed to the animation manager. The manager reads these\n// fields on every call, so mutating them in place (see the prop watches\n// below) propagates live setting changes without rebuilding the manager.\nconst animationDefaults = {\n  mixDuration: props.defaultMixDuration,\n  idleAnimationEnabled: props.idleAnimationEnabled,\n}\n\nconst canvas = toRef(() => props.canvas)\nconst modelSrc = toRef(() => props.modelSrc)\nconst paused = toRef(() => props.paused)\n\nfunction disposeSpine() {\n  if (spineCanvas) {\n    try {\n      spineCanvas.dispose()\n    }\n    catch (err) {\n      console.warn('[Spine] Failed to dispose SpineCanvas:', err)\n    }\n    spineCanvas = undefined\n  }\n  assetCleanup?.()\n  assetCleanup = undefined\n  animationManager = undefined\n  skeleton = undefined\n  animationState = undefined\n  modelIntrinsicBounds = undefined\n  spineStore.isModelLoaded = false\n}\n\nasync function loadModel() {\n  await modelLoadMutex.acquire()\n\n  modelLoading.value = true\n  componentState.value = 'loading'\n","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui-spine/src/components/scenes/spine/Model.vue#L83-L119","documentation":"disposeSpine() wraps spineCanvas.dispose() in try/catch; the dispose threw — commonly after WebGL context loss or when the update loop still touches resources. After the catch, every reference (assetCleanup, skeleton, animationState, bounds) is cleared and the store's isModelLoaded resets, so component state stays consistent though GPU resources may leak.","triggerScenarios":"Unmounting or disposing while the render loop still updates the skeleton; WebGL context already lost; spine runtime version mismatch with exported assets.","commonSituations":"Scene switching during unmount; tab suspended then resumed with a lost context; spine runtime upgraded without re-exporting assets.","solutions":["Accept the warning if the app continues normally — state refs are reset regardless","Stop the render/update loop before disposing the canvas","Handle webglcontextlost early; dispose-after-loss commonly throws","Keep the spine runtime version in sync with the asset exporter version"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"if (!spineCanvas) return // source guard in disposeSpine","typeGuard":null,"tryCatchPattern":"try {\n  spineCanvas.dispose()\n}\ncatch (err) {\n  console.warn('[Spine] Failed to dispose SpineCanvas:', err)\n}\n// always reset refs and store flags afterwards (source pattern) so state stays consistent","preventionTips":["Stop the render/update loop before dispose() so updates never touch freed resources","Handle webglcontextlost up front; dispose after context loss commonly throws","Keep the spine runtime version matched to the asset exporter version"],"tags":["spine","dispose","webgl","cleanup"],"backgroundTag":"webgl-context-lost","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}