Skip to content

feat(core): add editor destroy lifecycle - #196

Open
gohabereg wants to merge 4 commits into
feat/editor-destroyfrom
feat/editor-destroy-lifecycle
Open

gohabereg wants to merge 4 commits into
feat/editor-destroyfrom
feat/editor-destroy-lifecycle

Conversation

@gohabereg

@gohabereg gohabereg commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Why

First implementation step of the editor-destroy OpenSpec change (spec PR #195, the base of this one). Adds the editor teardown lifecycle and the API changes it needs; the adapters/UI/plugins side follows in a stacked PR.

What Changes

  • Core#destroy(): plugins in reverse construction order (destroy?.() + PluginRegistry.unregister), then UndoRedoManager, SelectionManager, BlockRenderer, then the adapter (destroy?.() — it becomes required in the next PR together with DOMAdapters.destroy()). Each step runs through a helper that logs and continues. Services resolved in initialize() are recorded in #services, so teardown never constructs one. A second call is a no-op because instances and services are cleared. use()/initialize() throw afterwards.
  • Interrupted initialize() rejects with a DOMException named AbortError instead of booting the document or dispatching core:ready.
  • EditorJS#destroy() delegates to Core. The bundle attaches reportUnlessAborted() to its ready promise: AbortError is ignored, other init errors are logged, so nothing is reported as an unhandled rejection; awaiting isReady still rejects.
  • BlockRenderer keeps each block's tool instance; destroys it on block removal and on teardown. A render that settles after teardown dispatches nothing.
  • Tool contracts: optional destroy() on BlockTool/InlineTool. Throwaway inline instances (SelectionManager.applyInlineTool, ToolsManager validation) are destroyed right after use.
  • Listeners through AbortController: onUpdate/onCaretUpdate subscribe through their own controller (signatures unchanged); UndoRedoManager, SelectionManager, BlockRenderer abort one controller each; BlockToolAdapter owns its controller and exposes protected signal to subclasses. EditorJSModel's typed addEventListener overloads accept options.

Verification

  • Full workspace build, lint and tests pass.
  • Mutation testing on every line this PR adds (Stryker scoped to the diff, -w): all mutants killed in core, and in sdk/editorjs via an ad-hoc run (those packages have no Stryker config). model changes are type-only. Packages' own yarn test:mutations pass.
  • Tests added for teardown order, error logging, idempotency, destroy before/during initialization, late renders, tool instance destruction, the AbortError handling (including that it is not left unhandled), and ToolsManager validation.

Notes

  • ToolsManager's inline-tool render check is pinned by tests as-is; removing it is tracked on the board ("Remove inverted inline tool render check in ToolsManager").
  • The integration tasks (real DOM, several editors per page) are covered by the e2e suite in feat: implement destroy() in adapters and plugins #207.

Stack

  1. docs(openspec): propose editor-destroy #195 spec
  2. This PR
  3. feat: implement destroy() in adapters and plugins #207 adapters, plugins, e2e tests and the archive (stacked on this one)

🤖 Generated with Claude Code

gohabereg and others added 3 commits October 6, 2026 18:13
- Core#destroy(): destroys plugins in reverse construction order and
  unregisters their public APIs, then UndoRedoManager, SelectionManager,
  BlockRenderer and the adapter. Idempotent, safe before and during
  initialize(), never mutates the model. use()/initialize() throw after it.
- EditorJS#destroy() delegates to Core.
- BlockRenderer keeps each block's tool instance and destroys it on block
  removal and on teardown; a render settling after teardown is dropped.
- Optional destroy() on BlockTool/InlineTool; throwaway inline tool
  instances (SelectionManager, ToolsManager validation) are destroyed
  right after use.
- Listeners are released through AbortController: onUpdate/onCaretUpdate
  subscribe through their own controller, BlockToolAdapter owns one and
  exposes a protected signal to subclasses, core services abort theirs
  on destroy. Model addEventListener overloads accept options.

Part of the editor-destroy OpenSpec change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Cover teardown error logging, services that were never resolved, an
  adapter without destroy(), and blocks removed without a tracked tool.
- Cover ToolsManager inline tool validation around the destroyed
  validation instance.
- Drop the redundant destroyed guard in Core.destroy(): clearing plugin
  instances and services already makes a second call a no-op.
- Drop { once: true } on the BlockToolAdapter abort listener: a signal
  aborts only once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tError

- Core.initialize() rejects with a DOMException named AbortError when
  destroy() runs while tools are being prepared, instead of resolving.
- The EditorJS bundle handles its own ready promise: an AbortError is
  ignored, any other initialization error is logged, so neither is
  reported as an unhandled rejection. Awaiting isReady still rejects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Unit Tests

Package Coverage Delta
@editorjs/shortcuts-plugin 100% 0% ⚪️
@editorjs/collaboration-manager 85.81% 0% ⚪️
@editorjs/clipboard-plugin 66.66% 0% ⚪️
@editorjs/dom-adapters 86.95% 0% ⚪️
@editorjs/editorjs 100% 0% ⚪️
@editorjs/ot-server 20% 0% ⚪️
@editorjs/model 98.48% 0% ⚪️
@editorjs/core 76.71% +3.57% 🟢
@editorjs/model-types N/A N/A

Mutation Tests

Package Mutation score Dashboard URL
@editorjs/shortcuts-plugin No files to mutate found.
@editorjs/clipboard-plugin No files to mutate found.
@editorjs/dom-adapters No files to mutate found.
@editorjs/model NaN% Dashboard
@editorjs/collaboration-manager No files to mutate found.
@editorjs/core 72.75% 🟡 Dashboard

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant