{"record":{"id":"de978832e38328dc","repo":"siyuan-note/siyuan","slug":"recorder-has-been-disposed","errorCode":null,"errorMessage":"Recorder has been disposed","messagePattern":"Recorder has been disposed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/protyle/util/RecordMedia.ts","lineNumber":114,"sourceCode":"        }\n\n        const contextOptions = {} as AudioContextOptions & { sinkId?: { type: \"none\" } };\n        if ((isInAndroid() || isInHarmony()) && \"setSinkId\" in AudioContextConstructor.prototype) {\n            contextOptions.sinkId = {type: \"none\"};\n        }\n        this.context = new AudioContextConstructor(contextOptions);\n        if (!this.context.audioWorklet || typeof AudioWorkletNode === \"undefined\") {\n            throw new Error(\"AudioWorklet is not supported\");\n        }\n        this.audioInput = this.context.createMediaStreamSource(mediaStream);\n        this.mediaStream.getAudioTracks().forEach((track) => {\n            track.addEventListener(\"ended\", this.handleTrackEnded);\n        });\n    }\n\n    public async startRecording() {\n        if (this.disposed) {\n            throw new Error(\"Recorder has been disposed\");\n        }\n        if (this.isRecording) {\n            return;\n        }\n\n        this.chunks = [];\n        this.readyPromise = new Promise<void>((resolve, reject) => {\n            this.resolveReady = resolve;\n            this.rejectReady = reject;\n        });\n        // Webpack 通过 import.meta.url 将录音编码器打包为按需加载的独立 Worker。\n        // @ts-ignore TypeScript 的 CommonJS 类型检查不识别由 Webpack 转换的 import.meta.url。\n        this.worker = new Worker(new URL(\"./RecordMediaWorker.ts\", import.meta.url));\n        this.worker.onmessage = (event: MessageEvent<EncoderMessage>) => {\n            this.handleWorkerMessage(event.data);\n        };\n        this.worker.onerror = (event: ErrorEvent) => {\n            this.handleWorkerError(new Error(event.message || \"MP3 encoder failed\"));","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/app/src/protyle/util/RecordMedia.ts#L96-L132","documentation":"Thrown by RecordMedia.startRecording when the recorder instance has already been disposed. Once dispose() runs, its resources (AudioContext, tracks) are released, so starting a new recording on the dead instance is refused instead of failing obscurely later. A new RecordMedia instance must be created for the next recording.","triggerScenarios":"Calling await recorder.startRecording() after recorder.dispose() has been called — e.g. a stale UI handler or a queued promise resolving after the recording dialog was closed.","commonSituations":"User closes the recording dialog while a start action is in flight; components caching a recorder instance across sessions; event listeners not removed on teardown firing after dispose.","solutions":["Guard calls: check the instance is not disposed (or track disposal in your wrapper) before calling startRecording","Create a fresh RecordMedia instance for each recording session instead of reusing a disposed one","Remove/nullify references and event handlers to the recorder when disposing so stale callers cannot reach it","Catch the error and restart the flow with a newly constructed recorder"],"exampleFix":"// before\nawait cachedRecorder.startRecording();\n// after\nif (cachedRecorder?.disposed) { cachedRecorder = new RecordMedia(stream); }\nawait cachedRecorder.startRecording();","handlingStrategy":"try-catch","validationCode":"// caller-side bookkeeping\nlet disposed = false;\nconst isAlive = () => recorder != null && !disposed;","typeGuard":null,"tryCatchPattern":"try {\n  await recorder.startRecording();\n} catch (e) {\n  if (e.message === \"Recorder has been disposed\") {\n    recorder = new RecordMedia(stream); // restart with a fresh instance\n    await recorder.startRecording();\n  } else throw e;\n}","preventionTips":["Create a new RecordMedia per recording session; never reuse after dispose","Null out references and remove event listeners when disposing","Disable record buttons once disposal has begun","Await startRecording before allowing teardown"],"tags":["audio","lifecycle","disposed"],"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-23T08:17:48.524Z"}