{"record":{"id":"ed521206790bc7d5","repo":"moeru-ai/airi","slug":"extension-directory-importer-is-disposed","errorCode":null,"errorMessage":"Extension directory importer is disposed.","messagePattern":"Extension directory importer is disposed\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts","lineNumber":446,"sourceCode":"    return await result\n  }\n\n  /** Cancels one prepared plan without changing the managed Extension folder. */\n  cancel(planId: string): void {\n    this.plans.delete(planId)\n  }\n\n  /** Stops new work and waits for queued imports during host shutdown. */\n  async dispose(): Promise<void> {\n    this.disposed = true\n    this.plans.clear()\n    await this.initialization\n    await this.commitQueue\n  }\n\n  private assertActive(): void {\n    if (this.disposed) {\n      throw new Error('Extension directory importer is disposed.')\n    }\n  }\n\n  private async withSecurityScopedAccess<TResult>(\n    bookmark: string | undefined,\n    operation: () => Promise<TResult>,\n  ): Promise<TResult> {\n    if (!bookmark) {\n      return await operation()\n    }\n    if (!this.startAccessingSecurityScopedResource) {\n      throw new Error('Security-scoped Extension import access is not configured.')\n    }\n\n    const stopAccessing = this.startAccessingSecurityScopedResource(bookmark)\n    try {\n      return await operation()\n    }","sourceCodeStart":428,"sourceCodeEnd":464,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts#L428-L464","documentation":"The ExtensionDirectoryImporter service has a lifecycle: after dispose() is called it must not be used again. assertActive() is a guard invoked on every public operation (initialize, prepare, and paths leading to commit) that throws this error if this.disposed is true. It prevents reads/writes to a torn-down importer whose resources (queues, security-scoped access) may already be released.","triggerScenarios":"Calling initialize() or prepare() (or any other guarded method) on the importer after dispose() has completed; the error is thrown synchronously by assertActive at directory-import.ts:446. Common flow: a window/page closes and disposes the importer, but a pending UI action still calls prepare().","commonSituations":"Double-invocation of a close/cleanup handler that disposes the importer twice; a Vue component unmount disposing the service while an in-flight dialog callback later calls prepare(); hot module reload recreating state but stale references calling the old disposed instance; calling the importer from a stale IPC handler after teardown.","solutions":["Do not call initialize/prepare/commit after dispose(); check your app lifecycle so teardown happens last","Guard call sites: only invoke importer methods while the owning component/window is mounted","Re-create the importer instance if it was disposed and you need to import again","If a pending operation should survive dispose, await this.initialization/this.commitQueue before disposing instead of racing them"],"exampleFix":"// before\n// importer.dispose()\n// await importer.prepare(bookmark) // throws 'disposed'\n// after\n// if (!importer.isDisposed) {\n//   await importer.prepare(bookmark)\n// }\n// or recreate: const importer = createExtensionDirectoryImporter(); await importer.prepare(bookmark)","handlingStrategy":"try-catch","validationCode":"// Track disposal in the caller and refuse calls after teardown\nlet disposed = false\nfunction disposeImporter() {\n  disposed = true\n  importer.dispose()\n}\nfunction prepareSafe(bookmark: string) {\n  if (disposed) return Promise.reject(new Error('Importer already disposed'))\n  return importer.prepare(bookmark)\n}","typeGuard":"function canUseImporter(importer: { disposed: boolean } | null | undefined): importer is { disposed: false } {\n  return importer != null && !importer.disposed\n}","tryCatchPattern":"try {\n  await importer.initialize()\n  await importer.prepare(bookmark)\n}\ncatch (err) {\n  if (errorMessageFrom(err)?.includes('importer is disposed')) {\n    // recreate a fresh instance instead of retrying the disposed one\n    importer = createExtensionDirectoryImporter()\n    await importer.initialize()\n  }\n  else throw err\n}","preventionTips":["Dispose the importer only in a single teardown path (e.g. onUnmounted) to avoid double-dispose races","Guard UI callbacks so they cannot invoke the importer after unmount or window close","Recreate the importer instance whenever a disposed one is needed again","Ensure teardown awaits pending initialization/commit work before disposing"],"tags":["lifecycle","disposed","invalid-state","extension-import"],"backgroundTag":"invalid-state-transition","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-09-17T01:14:42.644Z","contentChangedAt":"2026-09-17T01:14:42.644Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}