fix(types): type SuperDoc config callback bridge + correct callback payload shapes (SD-673)#3503
Merged
Merged
Conversation
…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).
Contributor
There was a problem hiding this comment.
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
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 Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
|
🎉 This PR is included in superdoc-cli v0.13.0 The release is available on GitHub release |
Contributor
|
🎉 This PR is included in superdoc-sdk v1.12.0 |
Contributor
|
🎉 This PR is included in @superdoc-dev/mcp v0.8.0 The release is available on GitHub release |
Contributor
|
🎉 This PR is included in superdoc v1.36.0 The release is available on GitHub release |
Contributor
|
🎉 This PR is included in @superdoc-dev/react v1.7.0 The release is available on GitHub release |
This was referenced May 27, 2026
Closed
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the
any-castasEventListenerbridge with a typed#onConfig<K extends keyof SuperDocEventMap>(event, listener)helper. The previous bridge letConfig.onXcallbacks register against runtime events without type-checking the payload match, which masked several incorrectConfigtypes 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
#onConfignow ignores explicitundefinedcallbacks instead of registering them as event listeners. Consumers who were destructuring against the previous wrong shapes (e.g.(p) => p.contextononAwarenessUpdate) will get TS errors that point at real runtime bugs already in their code.Callback contract fixes:
Config.onLocked— was{ isLocked, lockedBy: User }; nowSuperDocLockedPayload(lockedBy: User | null, non-optional).Config.onEditorCreate/Config.onEditorBeforeCreate/Config.onCollaborationReady— were bareEditor; nowSuperDocEditorPayload({ editor }). The runtime always wraps; bareEditornever matched.Config.onCommentsUpdate— was{ type: string; data: object }; nowSuperDocCommentsUpdatePayload({ type, comment?, changes? }). Runtime never emits adatafield.Config.onAwarenessUpdate— was{ context: SuperDoc; states }; nowSuperDocAwarenessUpdatePayload({ states, added, removed, superdoc }). Field rename plus two missing fields.Config.onListDefinitionsChange— was(params: {}); now(params: ListDefinitionsPayload).Config.onReady— parameter was namededitorwhile typed{ superdoc }; renamed toparamsand typed againstSuperDocReadyPayload.EditorUpdateEventreconciled —editor/sourceEditormade optional (runtime can produce undefined when both are missing);headerId/sectionTypemade requiredstring | null(runtime payload builder always sets them, defaulting tonull).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#onConfigdocblock 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 toall-public-typesAssertNotAnyand the root-classification snapshot as supported-root entries.Out of scope (deferred to follow-up PRs):
PermissionResolverParamsexport, JSDoc hygiene scanner,.tsJSDoc 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).