@@ -64,6 +64,7 @@ const DEFAULT_AWARENESS_PALETTE = Object.freeze([
6464/** @typedef {import('./types/index.js').Editor } Editor */
6565/** @typedef {import('./types/index.js').DocumentMode } DocumentMode */
6666/** @typedef {import('./types/index.js').Config } Config */
67+ /** @typedef {import('./types/index.js').InternalConfig } InternalConfig */
6768/** @typedef {import('./types/index.js').ExportParams } ExportParams */
6869/** @typedef {import('./types/index.js').UpgradeToCollaborationOptions } UpgradeToCollaborationOptions */
6970/** @typedef {import('./types/index.js').SurfaceRequest } SurfaceRequest */
@@ -121,6 +122,38 @@ export class SuperDoc extends EventEmitter {
121122 */
122123 colors = [ ] ;
123124
125+ /**
126+ * Pinia stores and Vue runtime references. Populated by `#initVueApp`
127+ * inside the async `#init`, which runs *after* `await #initCollaboration`,
128+ * so these fields are briefly `undefined` between `new SuperDoc(config)`
129+ * returning and the `ready` event firing. The non-null JSDoc here matches
130+ * the existing pattern used for `users` / `version` / `whiteboard` and
131+ * assumes consumers wait for `ready` before dereferencing. SD-2916 tracks
132+ * the systematic soundness fix across all of these fields (declaring them
133+ * `T | undefined` and casting at internal post-init access sites).
134+ *
135+ * @type {ReturnType<typeof import('../stores/superdoc-store.js').useSuperdocStore> }
136+ */
137+ superdocStore ;
138+
139+ /** @type {ReturnType<typeof import('../stores/comments-store.js').useCommentsStore> } */
140+ commentsStore ;
141+
142+ /** @type {ReturnType<typeof import('../composables/use-high-contrast-mode.js').useHighContrastMode> } */
143+ highContrastModeStore ;
144+
145+ /** @type {import('vue').App } */
146+ app ;
147+
148+ /** @type {import('pinia').Pinia } */
149+ pinia ;
150+
151+ /** @type {number } Count of editors that have signaled `editorCreate`. */
152+ readyEditors = 0 ;
153+
154+ /** @type {number } Outstanding async saves waiting for collaboration ack. */
155+ pendingCollaborationSaves = 0 ;
156+
124157 /** @type {Config } */
125158 config = {
126159 superdocId : null ,
@@ -240,6 +273,16 @@ export class SuperDoc extends EventEmitter {
240273 }
241274 normalizeTrackChangesConfig ( this . config ) ;
242275
276+ // Defensive defaults so the `InternalConfig` runtime invariants hold
277+ // for every reachable code path. The class-field initializer seeds
278+ // both `documents: []` and `layoutEngineOptions` is filled in by
279+ // `normalizeTrackChangesConfig` above, but a consumer that explicitly
280+ // passes `{ documents: undefined }` or omits `layoutEngineOptions`
281+ // when track-changes hasn't initialized it yet would otherwise leave
282+ // these undefined and break later non-null casts.
283+ this . config . documents = this . config . documents || [ ] ;
284+ this . config . layoutEngineOptions = this . config . layoutEngineOptions || { } ;
285+
243286 // Web layout behavior:
244287 // - Backward compatible default: web layout still uses PM rendering.
245288 // - Opt-in semantic path: allow layout engine only when flowMode === 'semantic'.
@@ -503,7 +546,7 @@ export class SuperDoc extends EventEmitter {
503546 this . superdocStore . setExceptionHandler ( ( /** @type {unknown } */ payload ) => this . emit ( 'exception' , payload ) ) ;
504547 }
505548 this . superdocStore . init ( this . config ) ;
506- const commentsModuleConfig = this . config . modules . comments ;
549+ const commentsModuleConfig = /** @type { InternalConfig } */ ( this . config ) . modules . comments ;
507550 // `commentsModuleConfig` is `false | object | undefined`. A truthy
508551 // check already rules out both `false` and `undefined`, so an
509552 // explicit `!== false` afterwards is redundant.
@@ -629,7 +672,7 @@ export class SuperDoc extends EventEmitter {
629672 this . #assignUserColor( ) ;
630673 this . _cleanupAwareness = setupAwarenessHandler ( provider , this , this . config . user ) ;
631674
632- this . config . documents . forEach ( ( doc ) => {
675+ /** @type { InternalConfig } */ ( this . config ) . documents . forEach ( ( doc ) => {
633676 doc . ydoc = ydoc ;
634677 doc . provider = provider ;
635678 doc . role = this . config . role ;
@@ -652,9 +695,10 @@ export class SuperDoc extends EventEmitter {
652695 this . _commentsCollabInitialized = false ;
653696 this . ydoc = undefined ;
654697 this . provider = undefined ;
655- delete this . config . modules . collaboration ;
698+ const cfg = /** @type {InternalConfig } */ ( this . config ) ;
699+ delete cfg . modules . collaboration ;
656700
657- this . config . documents . forEach ( ( doc ) => {
701+ cfg . documents . forEach ( ( doc ) => {
658702 delete doc . ydoc ;
659703 delete doc . provider ;
660704 } ) ;
@@ -722,7 +766,7 @@ export class SuperDoc extends EventEmitter {
722766 overwriteRoomLockState ( ydoc , { isLocked : this . isLocked , lockedBy : this . lockedBy } ) ;
723767
724768 // --- Attach collaboration config (awareness, flags, config.documents) ---
725- this . config . modules . collaboration = { ydoc, provider } ;
769+ /** @type { InternalConfig } */ ( this . config ) . modules . collaboration = { ydoc, provider } ;
726770 this . #attachExternalCollaboration( ydoc , provider ) ;
727771
728772 // --- Update live store documents in place (no Vue unmount) ---
@@ -967,14 +1011,15 @@ export class SuperDoc extends EventEmitter {
9671011 throw new Error ( 'SuperDoc: upgradeToCollaboration() requires both ydoc and provider' ) ;
9681012 }
9691013
970- const docxDocs = this . config . documents . filter ( ( d ) => d . type === DOCX ) ;
1014+ const cfg = /** @type {InternalConfig } */ ( this . config ) ;
1015+ const docxDocs = cfg . documents . filter ( ( d ) => d . type === DOCX ) ;
9711016 if ( docxDocs . length === 0 ) {
9721017 throw new Error ( 'SuperDoc: no DOCX document found for upgrade' ) ;
9731018 }
9741019 if ( docxDocs . length > 1 ) {
9751020 throw new Error ( 'SuperDoc: upgradeToCollaboration() only supports a single DOCX document' ) ;
9761021 }
977- if ( this . config . documents . length !== docxDocs . length ) {
1022+ if ( cfg . documents . length !== docxDocs . length ) {
9781023 throw new Error ( 'SuperDoc: upgradeToCollaboration() only supports single-DOCX instances' ) ;
9791024 }
9801025 }
@@ -986,7 +1031,12 @@ export class SuperDoc extends EventEmitter {
9861031 * @throws {Error } If the editor is not yet created
9871032 */
9881033 #resolveSourceEditor( ) {
989- const docxDoc = this . config . documents . find ( ( d ) => d . type === DOCX ) ;
1034+ // Upstream `#assertCanUpgrade` already verified at least one DOCX
1035+ // document exists; cast the find result to assert non-null without
1036+ // changing runtime behavior.
1037+ const docxDoc = /** @type {Document } */ (
1038+ /** @type {InternalConfig } */ ( this . config ) . documents . find ( ( d ) => d . type === DOCX )
1039+ ) ;
9901040 const storeDoc = this . superdocStore . documents . find ( ( d ) => d . id === docxDoc . id ) ;
9911041 const editor = storeDoc ?. getEditor ?. ( ) ;
9921042
@@ -1021,7 +1071,10 @@ export class SuperDoc extends EventEmitter {
10211071 */
10221072 onContentError ( { error, editor } ) {
10231073 const { documentId } = editor . options ;
1024- const doc = this . superdocStore . documents . find ( ( d ) => d . id === documentId ) ;
1074+ // The errored editor came from `superdocStore.documents`, so the find
1075+ // by its `documentId` is expected to hit. Cast the find result to a
1076+ // RuntimeDocument to assert non-null at the consumer callback.
1077+ const doc = /** @type {RuntimeDocument } */ ( this . superdocStore . documents . find ( ( d ) => d . id === documentId ) ) ;
10251078 // `onContentError` is typed as optional on the public Config typedef
10261079 // because consumers don't have to wire a handler. The class field
10271080 // initializer installs a `() => null` default, but `#init` spreads
@@ -1549,7 +1602,7 @@ export class SuperDoc extends EventEmitter {
15491602 * @param {boolean } lock
15501603 */
15511604 setLocked ( lock = true ) {
1552- this . config . documents . forEach ( ( doc ) => {
1605+ /** @type { InternalConfig } */ ( this . config ) . documents . forEach ( ( doc ) => {
15531606 // setLocked is a collaboration-only API; the surrounding flow only
15541607 // calls it once each document has a Yjs doc attached. Cast away the
15551608 // optional shape on the public Document typedef without changing
@@ -1777,15 +1830,16 @@ export class SuperDoc extends EventEmitter {
17771830 this . _cleanupAwareness = null ;
17781831 }
17791832
1780- this . config . socket ?. cancelWebsocketRetry ( ) ;
1781- this . config . socket ?. disconnect ( ) ;
1782- this . config . socket ?. destroy ( ) ;
1833+ const cfg = /** @type {InternalConfig } */ ( this . config ) ;
1834+ cfg . socket ?. cancelWebsocketRetry ( ) ;
1835+ cfg . socket ?. disconnect ( ) ;
1836+ cfg . socket ?. destroy ( ) ;
17831837
17841838 this . ydoc ?. destroy ( ) ;
17851839 this . provider ?. disconnect ( ) ;
17861840 this . provider ?. destroy ( ) ;
17871841
1788- this . config . documents . forEach ( ( doc ) => {
1842+ cfg . documents . forEach ( ( doc ) => {
17891843 doc . provider ?. disconnect ( ) ;
17901844 doc . provider ?. destroy ( ) ;
17911845 doc . ydoc ?. destroy ( ) ;
0 commit comments