{"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":"Error","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/251596fc0de2f9528c00c224252fd073a99973f4/app/src/protyle/util/RecordMedia.ts#L96-L132","documentation":"startRecording() checks the disposed flag at entry and throws 'Recorder has been disposed' if dispose() was already called. The recorder cannot be reused once disposed — context, recorder, worker, and tracks have all been torn down.","triggerScenarios":"Calling startRecording() after dispose() — for example, a UI that lets the user start a new recording on the same RecordMedia instance after stop/dispose, or a race where the close handler disposes before the start button handler runs.","commonSituations":"Single-use instance accidentally reused; stop then immediately start without constructing a new RecordMedia; dialog teardown disposes the recorder while a pending start is queued.","solutions":["Treat RecordMedia as single-use: build a new instance (new RecordMedia(stream)) for every recording session.","Track a started flag in the UI and disable the start button after dispose().","Reset the reference to null after dispose() so accidental reuse fails fast at the call site.","Audit dialog close/confirm handlers to ensure they do not dispose a recorder that the start handler still references."],"exampleFix":"// before\nrecordMedia.dispose();\nawait recordMedia.startRecording(); // throws\n// after\nrecordMedia.dispose();\nrecordMedia = null;\n// caller constructs a fresh instance when needed\nrecordMedia = new RecordMedia(await navigator.mediaDevices.getUserMedia({audio: true}));","handlingStrategy":"validation","validationCode":"if (!recordMedia || recordMedia['disposed']) throw new Error('Recorder already disposed; create a new instance');","typeGuard":null,"tryCatchPattern":"try { await recordMedia.startRecording(); }\ncatch (e) { if (/disposed/i.test(e.message)) { recordMedia = null; /* build new */ } else throw e; }","preventionTips":["Treat RecordMedia as single-use; construct a new instance per recording.","Set the reference to null after dispose() so reuse fails fast at the call site.","Disable the start button once dispose() runs.","Audit dialog teardown so it does not dispose a recorder still referenced by a start handler."],"tags":["web-audio","recording","lifecycle","typescript"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}