Skip to content

fix(types): type SuperDoc config callback bridge + correct callback payload shapes (SD-673)#3503

Merged
caio-pizzol merged 3 commits into
mainfrom
caio-pizzol/SD-typed-callback-bridge
May 26, 2026
Merged

fix(types): type SuperDoc config callback bridge + correct callback payload shapes (SD-673)#3503
caio-pizzol merged 3 commits into
mainfrom
caio-pizzol/SD-typed-callback-bridge

Conversation

@caio-pizzol

Copy link
Copy Markdown
Contributor

Replaces the any-cast asEventListener bridge with a typed #onConfig<K extends keyof SuperDocEventMap>(event, listener) helper. The previous bridge let Config.onX callbacks register against runtime events without type-checking the payload match, which masked several incorrect Config types that drifted from what the runtime actually emits.

These are type contract corrections to match existing runtime behavior; no intended runtime behavior change except that #onConfig now ignores explicit undefined callbacks instead of registering them as event listeners. Consumers who were destructuring against the previous wrong shapes (e.g. (p) => p.context on onAwarenessUpdate) will get TS errors that point at real runtime bugs already in their code.

Callback contract fixes:

  • Config.onLocked — was { isLocked, lockedBy: User }; now SuperDocLockedPayload (lockedBy: User | null, non-optional).
  • Config.onEditorCreate / Config.onEditorBeforeCreate / Config.onCollaborationReady — were bare Editor; now SuperDocEditorPayload ({ editor }). The runtime always wraps; bare Editor never matched.
  • Config.onCommentsUpdate — was { type: string; data: object }; now SuperDocCommentsUpdatePayload ({ type, comment?, changes? }). Runtime never emits a data field.
  • Config.onAwarenessUpdate — was { context: SuperDoc; states }; now SuperDocAwarenessUpdatePayload ({ states, added, removed, superdoc }). Field rename plus two missing fields.
  • Config.onListDefinitionsChange — was (params: {}); now (params: ListDefinitionsPayload).
  • Config.onReady — parameter was named editor while typed { superdoc }; renamed to params and typed against SuperDocReadyPayload.
  • EditorUpdateEvent reconciled — editor / sourceEditor made optional (runtime can produce undefined when both are missing); headerId / sectionType made required string | null (runtime payload builder always sets them, defaulting to null).

One subtlety worth flagging: the typed bridge alone did not catch onListDefinitionsChange{} is contravariantly assignable to any narrower payload, so TypeScript accepted the wrong shape at the registration site. The consumer fixture (tests/consumer-typecheck/src/config-callback-payloads.ts) caught it. The #onConfig docblock now explicitly calls out this limit. This is concrete evidence that the bridge and the fixtures protect against different bug classes.

New named payload types exported through the public facade: SuperDocReadyPayload, SuperDocEditorPayload, SuperDocLockedPayload, SuperDocCommentsUpdatePayload, SuperDocAwarenessUpdatePayload. Added to all-public-types AssertNotAny and the root-classification snapshot as supported-root entries.

Out of scope (deferred to follow-up PRs): PermissionResolverParams export, JSDoc hygiene scanner, .ts JSDoc cleanup.

Verified: pnpm check:types -> PASS; pnpm check:public:superdoc --skip-build -> PASS (9 ran, 1 skipped, 127.7s); SuperDoc unit tests -> PASS (1054/1054).

…ayload shapes (SD-673)

Replaces the any-cast asEventListener bridge with a typed
#onConfig<K extends keyof SuperDocEventMap>(event, listener) helper.
The previous bridge let consumer Config.onX callbacks register against
runtime SuperDocEventMap events without type-checking the payload
match, which masked several incorrect Config types that drifted from
what the runtime actually emits.

These are type contract corrections to match existing runtime behavior;
no intended runtime behavior change except that #onConfig now ignores
explicit undefined callbacks instead of registering them as event
listeners.

Callback contract fixes (each is a breaking change for consumers who
were destructuring against the previous wrong shape):

- Config.onLocked: { isLocked, lockedBy: User } -> SuperDocLockedPayload
  (lockedBy is non-optional User | null; runtime always emits the key,
  value may be null on unlock or unattributed locks)
- Config.onEditorBeforeCreate / Config.onEditorCreate /
  Config.onCollaborationReady: bare Editor -> SuperDocEditorPayload
  (the runtime wraps as { editor }; bare Editor never matched runtime)
- Config.onCommentsUpdate: { type, data: object } ->
  SuperDocCommentsUpdatePayload ({ type, comment?, changes? }; runtime
  never emits a 'data' field)
- Config.onAwarenessUpdate: { context, states } ->
  SuperDocAwarenessUpdatePayload ({ states, added, removed, superdoc };
  field rename + 2 missing fields)
- Config.onListDefinitionsChange: (params: {}) ->
  (params: ListDefinitionsPayload). The typed bridge alone did not
  catch this: {} is contravariantly assignable, so it accepted the
  wrong shape; the consumer fixture caught it.
- Config.onReady: parameter named 'editor' but typed { superdoc };
  renamed to 'params' and typed against SuperDocReadyPayload.
- EditorUpdateEvent reconciled: editor/sourceEditor made optional
  (runtime can produce undefined when both are missing);
  headerId/sectionType made required string | null (runtime payload
  builder always sets them, defaulting to null).

New named payload types exported through the public facade:
SuperDocReadyPayload, SuperDocEditorPayload, SuperDocLockedPayload,
SuperDocCommentsUpdatePayload, SuperDocAwarenessUpdatePayload. Added
to all-public-types AssertNotAny and the root-classification snapshot
as supported-root entries.

Consumer fixture (config-callback-payloads.ts) locks the corrected
shapes via AssertEqual on Parameters<NonNullable<Config['onX']>>[0].

Verified: pnpm check:types -> PASS; pnpm check:public:superdoc
--skip-build -> PASS (9 ran, 1 skipped, 127.7s); SuperDoc unit tests
-> PASS (1054/1054).
@caio-pizzol
caio-pizzol requested a review from a team as a code owner May 26, 2026 11:43
@linear-code

linear-code Bot commented May 26, 2026

Copy link
Copy Markdown

SD-673

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cubic analysis

1 issue found across 10 files

Linked issue analysis

Linked issue: SD-673: TypeScript-first public API: SuperDoc and Document API

Status Acceptance criteria Notes
Replace the untyped any-cast EventEmitter bridge with a typed #onConfig helper that no-ops on undefined The PR removes the old asEventListener bridge and adds #onConfig(event, listener) which guards undefined and registers listeners via this.on(event, listener).
Correct Config callback payload shapes for onLocked, onEditorCreate/onEditorBeforeCreate/onCollaborationReady, onCommentsUpdate, onAwarenessUpdate, onListDefinitionsChange, and onReady The public payload types and Config callback signatures were updated to match runtime behavior (e.g. SuperDocLockedPayload, SuperDocEditorPayload, SuperDocCommentsUpdatePayload, SuperDocAwarenessUpdatePayload, ListDefinitionsPayload, SuperDocReadyPayload).
Reconcile EditorUpdateEvent fields to match runtime (editor/sourceEditor optional; headerId/sectionType string | null) EditorUpdateEvent was adjusted so editor and sourceEditor are optional and headerId/sectionType are required string | null, aligning types with runtime payload builder.
Export new named payload types through the public facade and include them in the all-public-types checks/root classification Named payload types were added to the public exports and the consumer typecheck artifacts were updated to include them in the supported-root and AssertNotAny fixtures.
Project type and public contract checks and unit tests pass after these changes Author reports type checks and public contract checks passed and unit tests are green, indicating the changes satisfy public-type expectations and consumer fixtures.

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread packages/superdoc/src/core/SuperDoc.ts Outdated
Pre-existing behavior: the toolbar exception bridge passed
this.config.onException directly to eventemitter3's .on(), which
throws TypeError('The listener must be a function') at registration
time when the value is undefined. Same issue existed pre-PR through
the old asEventListener identity cast; bot review on #3503 flagged it
during this PR's bridge migration.

Adds a truthy guard mirroring #onConfig's semantics: skip absent
callbacks (consumer explicitly passes { onException: undefined }),
but pass through truthy non-function values so eventemitter3 still
throws loudly for real type violations.

Verified: pnpm check:types -> PASS; pnpm --filter superdoc test --run
-> PASS (1054/1054); pnpm check:public:superdoc --skip-build -> PASS
(9 ran, 1 skipped).
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@caio-pizzol
caio-pizzol merged commit a495d34 into main May 26, 2026
39 of 56 checks passed
@caio-pizzol
caio-pizzol deleted the caio-pizzol/SD-typed-callback-bridge branch May 26, 2026 16:16
@superdoc-bot

superdoc-bot Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in superdoc-cli v0.13.0

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in superdoc-sdk v1.12.0

@superdoc-bot

superdoc-bot Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in @superdoc-dev/mcp v0.8.0

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in superdoc v1.36.0

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in @superdoc-dev/react v1.7.0

The release is available on GitHub release

mattConnHarbour pushed a commit to mattConnHarbour/superdoc that referenced this pull request May 28, 2026
Add key-set assertions for the six public events whose Config callbacks
have named payload types (introduced in superdoc-dev#3503). Existing tests used
objectContaining({...}) which would not catch a missing or extra field;
these new assertions pin Object.keys(payload).sort() to the exact
declared shape.

This protects the bug class found and fixed in superdoc-dev#3503: typed callback
payloads silently drifted from the runtime emit (onLocked,
onAwarenessUpdate, onCommentsUpdate had wrong shapes for months).

Events covered:
- ready                  -> { superdoc }                        (SuperDoc.test.js)
- editorBeforeCreate     -> { editor }                          (SuperDoc.test.js)
- editorCreate           -> { editor }                          (SuperDoc.test.js)
- locked                 -> { isLocked, lockedBy }              (SuperDoc.test.js)
- awareness-update       -> { states, added, removed, superdoc} (collaboration.test.js)
- comments-update        -> { type, comment }                   (comments-store.test.js)

Out of scope for this PR (pass-through events from upstream emitters,
runtime shape is already enforced by SuperDocEventMap in the consumer-
typecheck fixtures):
- collaboration-ready    (emitted from SuperDoc.vue verbatim)
- list-definitions-change (emitted from super-editor, re-emitted by
  SuperDoc.vue verbatim)

No new gate, no new scanner, no production code touched.

Verified:
- vitest run on the three touched files: 250/250 pass
- pnpm check:public:superdoc --skip-build -> PASS (11/12, 133.7s)
mattConnHarbour pushed a commit to mattConnHarbour/superdoc that referenced this pull request May 28, 2026
…examples (SD-673)

Adds a CI-deterministic gate that extracts 'Full Example' code blocks
from apps/docs/editor/superdoc/**, writes each snippet to a temp file
with a small shared ambient prelude, and runs tsc --noEmit --strict
against packages/superdoc/dist. JS fences are checked with allowJs +
checkJs + // @ts-check; TS fences are checked with full strict.

Why: the existing runtime doctest (apps/docs/__tests__/doctest.test.ts)
extracts the onReady body and executes it against a mocked superdoc
host, so it never catches destructure bugs in the outer config
example. The bug class fixed by superdoc-dev#3503 (typed callback payloads that
silently drifted) was teaching the wrong shape in docs for months —
nothing in CI caught it. This gate catches it at write time.

Mechanism:
- Extends lib/extract.ts to keep the fence language on each example.
- Adds yjs / y-websocket to SKIP_IMPORTS (consumer BYO; not part of
  SuperDoc's typed surface).
- New doctest-types.ts script: iterates pattern='superdoc' examples
  in scope, writes one temp .ts/.js per snippet plus a shared
  placeholders.d.ts (yourFile, doc1, doc2, cleanup, autoSave, etc.),
  runs tsc against the packed dist via tsconfig paths, parses errors
  and maps them back to source file:line.
- New apps/docs script entry: pnpm --filter @superdoc/docs run check:types
- Wired as the last stage of scripts/check-public-contract.mjs.

Fixes (split per the user's request between placeholder-only stubs
and real public-shape drift):

PLACEHOLDER STUBS (11 ambient declarations in placeholders.d.ts):
- File/string values: yourFile, file, doc1, doc2, content
- Helper functions: cleanup, autoSave, adjustLayout, showOnlineUsers,
  updateUserCursors, showLockBanner

REAL DOCS API DRIFT FIXES (in scope: editor/superdoc/** + the
collaboration configuration cell flagged in audit):

apps/docs/editor/superdoc/methods.mdx (29 examples touched):
- 22 onReady: (superdoc) => fixed to onReady: ({ superdoc }) =>
  (the typed callback payload is { superdoc }, not the instance)
- setActiveEditor example rewritten to capture editors via
  onEditorCreate (the previous example reached into runtime-only
  Document.editor which isn't on the public Document type)
- search / goToSearchResult: added null guards on the optional
  SearchMatch[] | undefined return
- addCommentsList: signature corrected to HTMLElement (was wrongly
  documented as string | HTMLElement; the actual type is HTMLElement)
- scrollToComment / scrollToElement: superdoc.editor -> superdoc.activeEditor
  with null guards (activeEditor is Editor | null on the typed surface;
  superdoc.editor doesn't exist as a public member)
- off example: handler annotated with /** @param SuperDocReadyPayload */
  so the standalone-handler shape pins to the typed payload
- scrollToElement: narrowed BlockNodeAddress before reading nodeId;
  switched .entityId to .id on DiscoveryItem (the canonical field name)

apps/docs/editor/superdoc/events.mdx (8 examples touched):
- Subscribing handler now uses JSDoc to type the standalone handler
- editor-update: added null guard on the optional editor field
  (EditorUpdateEvent.editor?: Editor)
- content-error: removed documentId from destructure
  (SuperDocContentErrorPayload is { error, editor }; no documentId)
- comments-update: { type, data } -> { type, comment, changes }
  (matches SuperDocCommentsUpdatePayload, the same drift class fixed
  in the typed surface by superdoc-dev#3503)
- awareness-update: { context, states } -> { states, added, removed }
  (same drift class; runtime emits 'superdoc' not 'context')
- locked: added && lockedBy guard before reading lockedBy.name
  (SuperDocLockedPayload.lockedBy is User | null)
- pagination-update: removed 'pagination: true' from Config example
  (no such field on the typed Config)
- exception: rewrote to take payload and read payload.error directly,
  with a comment on how to narrow the discriminated union (the union
  has three variants and 'document'/'editor' only exist on specific
  ones)

apps/docs/editor/superdoc/import-export.mdx (5 examples touched):
- 5 onReady: (superdoc) => fixed to onReady: ({ superdoc }) =>
- Added null guards on superdoc.activeEditor before .getHTML / .getJSON
  / .getMarkdown / .commands access
- Switched Usage tabs that show 'superdoc.activeEditor.getX()' to
  'superdoc.activeEditor?.getX()' for the same reason

apps/docs/editor/collaboration/configuration.mdx (1 cell):
- lockedBy type cell: 'Object' -> 'User | null' with a note that the
  example needs a null check before reading .name

Result:
- 47 in-scope examples (was 0 type-checked before this PR)
- 0 unmet — strict-zero from day 1 for this scope
- 1 yjs/y-websocket example skipped (out of scope: consumer BYO,
  not part of SuperDoc's typed surface)

Synthetic regression verified: re-introducing one onReady: (superdoc) =>
locally produced the expected failure 'Property getHTML does not exist
on type SuperDocReadyPayload' with the correct source file:line.

Verified:
- pnpm --filter @superdoc/docs run check:types -> OK, 47/47
- pnpm check:public:superdoc --skip-build -> PASS (12 ran, 1 skipped, 130.8s)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants