@@ -37,6 +37,7 @@ import type {
3737 EventResolverContext ,
3838 EventResolverKind ,
3939 GetInitialStorageFn ,
40+ IORoomDestroyedEvent ,
4041 IORoomListenerEvent ,
4142 IORoomMessageEvent ,
4243 IOUserConnectedEvent ,
@@ -64,7 +65,8 @@ export interface IORoomListeners<
6465 TContext extends Record < string , any > ,
6566 TEvents extends PluvRouterEventConfig < TPlatform , TAuthorize , TContext > ,
6667> {
67- onDestroy : ( event : IORoomListenerEvent < TPlatform , TContext > ) => void ;
68+ onRoomDestroyed : ( event : IORoomDestroyedEvent < TPlatform , TContext > ) => void ;
69+ onStorageDestroyed : ( event : IORoomListenerEvent < TPlatform , TContext > ) => void ;
6870 onMessage : ( event : IORoomMessageEvent < TPlatform , TAuthorize , TContext , TEvents > ) => void ;
6971 onUserConnected : ( event : IOUserConnectedEvent < TPlatform , TAuthorize , TContext > ) => void ;
7072 onUserDisconnected : ( event : IOUserDisconnectedEvent < TPlatform , TAuthorize , TContext > ) => void ;
@@ -81,11 +83,8 @@ export type BroadcastProxy<TIO extends IORoom<any, any, any, any, any>> = (<
8183
8284export type IORoomConfig <
8385 TPlatform extends AbstractPlatform < any > = AbstractPlatform < any > ,
84- TAuthorize extends PluvIOAuthorize <
85- TPlatform ,
86+ TAuthorize extends PluvIOAuthorize < TPlatform , any , InferInitContextType < TPlatform > > | null =
8687 any ,
87- InferInitContextType < TPlatform >
88- > | null = any ,
8988 TContext extends Record < string , any > = { } ,
9089 TEvents extends PluvRouterEventConfig < TPlatform , TAuthorize , TContext > = { } ,
9190> = Partial < IORoomListeners < TPlatform , TAuthorize , TContext , TEvents > > & {
@@ -118,16 +117,12 @@ export type WebSocketRegisterConfig<
118117
119118export class IORoom <
120119 TPlatform extends AbstractPlatform < any > = AbstractPlatform < any > ,
121- TAuthorize extends PluvIOAuthorize <
122- TPlatform ,
123- any ,
124- InferInitContextType < TPlatform >
125- > | null = null ,
120+ TAuthorize extends PluvIOAuthorize < TPlatform , any , InferInitContextType < TPlatform > > | null =
121+ null ,
126122 TContext extends Record < string , any > = { } ,
127123 TCrdt extends CrdtLibraryType < any > = CrdtLibraryType < any > ,
128124 TEvents extends PluvRouterEventConfig < TPlatform , TAuthorize , TContext > = { } ,
129- > implements IOLike < TAuthorize , TCrdt , TEvents >
130- {
125+ > implements IOLike < TAuthorize , TCrdt , TEvents > {
131126 public readonly id : string ;
132127
133128 private _doc : Promise < CrdtDocLike < any , any > > ;
@@ -150,6 +145,9 @@ export class IORoom<
150145 > ( ) ;
151146 private readonly _userSessionss = new Map < [ userId : string ] [ 0 ] , Set < [ sessionId : string ] [ 0 ] > > ( ) ;
152147
148+ private _wasDocEmptyOnInit : boolean = true ;
149+ private _storageInitializedViaSession : boolean = false ;
150+
153151 /**
154152 * @ignore
155153 * @readonly
@@ -200,7 +198,8 @@ export class IORoom<
200198 crdt = noop ,
201199 debug,
202200 getInitialStorage,
203- onDestroy,
201+ onRoomDestroyed,
202+ onStorageDestroyed,
204203 onMessage,
205204 onUserConnected,
206205 onUserDisconnected,
@@ -221,7 +220,8 @@ export class IORoom<
221220 this . _platform = platform . initialize ( { ...( ! ! _meta ? { _meta } : { } ) , roomContext } ) ;
222221
223222 this . _listeners = {
224- onDestroy : ( event ) => onDestroy ?.( event ) ,
223+ onRoomDestroyed : ( event ) => onRoomDestroyed ?.( event ) ,
224+ onStorageDestroyed : ( event ) => onStorageDestroyed ?.( event ) ,
225225 onMessage : ( event ) => onMessage ?.( event ) ,
226226 onUserConnected : ( event ) => onUserConnected ?.( event ) ,
227227 onUserDisconnected : ( event ) => onUserDisconnected ?.( event ) ,
@@ -674,7 +674,16 @@ export class IORoom<
674674 }
675675 }
676676
677- if ( typeof encodedState === "string" ) doc . applyEncodedState ( { update : encodedState } ) ;
677+ if ( typeof encodedState === "string" ) {
678+ doc . applyEncodedState ( { update : encodedState } ) ;
679+
680+ // If we loaded storage from persistence and it's non-empty, storage was previously initialized.
681+ // This handles hibernation wake-up: restore the initialization state so that onStorageDestroyed
682+ // will fire correctly when the room is destroyed.
683+ if ( ! doc . isEmpty ( ) ) {
684+ this . _storageInitializedViaSession = true ;
685+ }
686+ }
678687
679688 return doc ;
680689 }
@@ -797,6 +806,10 @@ export class IORoom<
797806 ) ;
798807
799808 const doc = await this . _getInitialDoc ( ) ;
809+
810+ // Track if doc was empty when room was first initialized
811+ this . _wasDocEmptyOnInit = doc . isEmpty ( ) ;
812+
800813 const uninitialize = async ( ) => {
801814 this . _platform . pubSub . unsubscribe ( pubSubId ) ;
802815
@@ -806,18 +819,35 @@ export class IORoom<
806819 doc . destroy ( ) ;
807820 this . _doc = Promise . resolve ( this . _docFactory . getEmpty ( ) ) ;
808821
822+ // Always emit onRoomDestroyed
809823 await Promise . resolve (
810- this . _listeners . onDestroy ( {
824+ this . _listeners . onRoomDestroyed ( {
811825 ...( "_meta" in this . _platform && ! ! this . _platform . _meta
812826 ? { _meta : this . _platform . _meta }
813827 : { } ) ,
814828 context,
815- encodedState,
816829 platform : this . _platform ,
817830 room : this . id ,
818831 } ) ,
819832 ) ;
820833
834+ // Only emit onStorageDestroyed if storage was initialized via initializeSession
835+ if ( this . _storageInitializedViaSession ) {
836+ await Promise . resolve (
837+ this . _listeners . onStorageDestroyed ( {
838+ ...( "_meta" in this . _platform && ! ! this . _platform . _meta
839+ ? { _meta : this . _platform . _meta }
840+ : { } ) ,
841+ context,
842+ encodedState,
843+ platform : this . _platform ,
844+ room : this . id ,
845+ } ) ,
846+ ) ;
847+
848+ this . _storageInitializedViaSession = false ;
849+ }
850+
821851 this . _uninitialize = null ;
822852 } ;
823853
@@ -991,6 +1021,10 @@ export class IORoom<
9911021
9921022 await Promise . all ( [ handleBroadcast ( ) , handleSelf ( ) , handleSync ( ) ] ) ;
9931023 } ) ;
1024+
1025+ // After processing messages that might update storage (like $initializeSession),
1026+ // check if storage was initialized via initializeSession
1027+ await this . _checkStorageInitializedViaSession ( ) ;
9941028 } ;
9951029 }
9961030
@@ -1061,6 +1095,19 @@ export class IORoom<
10611095 return set ;
10621096 }
10631097
1098+ private async _checkStorageInitializedViaSession ( ) : Promise < void > {
1099+ if ( this . _storageInitializedViaSession ) return ;
1100+
1101+ // Only mark as initialized if:
1102+ // 1. Doc was empty when room was first initialized
1103+ // 2. Doc is now non-empty
1104+ // 3. At least one session has been registered
1105+ const doc = await this . _doc ;
1106+ if ( this . _wasDocEmptyOnInit && ! doc . isEmpty ( ) && this . _sessions . size > 0 ) {
1107+ this . _storageInitializedViaSession = true ;
1108+ }
1109+ }
1110+
10641111 private async _sendMessage (
10651112 pluvWs : AbstractWebSocket < any , TAuthorize > ,
10661113 message : IOEventMessage < any > ,
0 commit comments