@@ -3,13 +3,36 @@ import type { SharedState, SharedStatePatch } from 'devframe/utils/shared-state'
33import type { DevframeRpcClient } from './rpc'
44import { createSharedState } from 'devframe/utils/shared-state'
55
6+ /**
7+ * Upper bound on remembered server-originated syncIds. An update's own
8+ * `updated` event fires synchronously after it is applied, so the set only
9+ * needs to outlive the brief window between applying a server update and
10+ * observing its emission — 100 comfortably covers a burst.
11+ */
12+ const MAX_REMOTE_SYNC_IDS = 100
13+
614export function createRpcSharedStateClientHost ( rpc : DevframeRpcClient ) : RpcSharedStateHost {
715 const sharedState = new Map < string , SharedState < any > > ( )
816 const stateDisposers = new Map < string , ( ) => void > ( )
917 const initialValues = new Map < string , any > ( )
1018 const keyAddedListeners = new Set < ( key : string ) => void > ( )
1119 const isStaticBackend = rpc . connectionMeta . backend === 'static'
1220
21+ // Server-originated syncIds, so the forwarding listener below can tell a
22+ // local mutation (forward it to the server) from an applied server update
23+ // (already the server's own — forwarding it back would be a pure echo the
24+ // server discards, at the cost of one wire message per update; over the
25+ // SSE transport that's a whole HTTP POST per server-side state tick).
26+ const remoteSyncIds = new Set < string > ( )
27+ function rememberRemoteSyncId ( syncId : string ) : void {
28+ remoteSyncIds . add ( syncId )
29+ if ( remoteSyncIds . size > MAX_REMOTE_SYNC_IDS ) {
30+ const oldest = remoteSyncIds . values ( ) . next ( ) . value
31+ if ( oldest !== undefined )
32+ remoteSyncIds . delete ( oldest )
33+ }
34+ }
35+
1336 function mergeWithInitialValue ( key : string , serverState : any ) : any {
1437 const initial = initialValues . get ( key )
1538 if ( initial && typeof initial === 'object' && ! Array . isArray ( initial )
@@ -26,6 +49,7 @@ export function createRpcSharedStateClientHost(rpc: DevframeRpcClient): RpcShare
2649 const state = sharedState . get ( key )
2750 if ( ! state || state . syncIds . has ( syncId ) )
2851 return
52+ rememberRemoteSyncId ( syncId )
2953 state . mutate ( ( ) => mergeWithInitialValue ( key , fullState ) , syncId )
3054 } ,
3155 } )
@@ -37,6 +61,7 @@ export function createRpcSharedStateClientHost(rpc: DevframeRpcClient): RpcShare
3761 const state = sharedState . get ( key )
3862 if ( ! state || state . syncIds . has ( syncId ) )
3963 return
64+ rememberRemoteSyncId ( syncId )
4065 state . patch ( patches , syncId )
4166 } ,
4267 } )
@@ -46,6 +71,9 @@ export function createRpcSharedStateClientHost(rpc: DevframeRpcClient): RpcShare
4671 offs . push ( state . on ( 'updated' , ( fullState , patches , syncId ) => {
4772 if ( isStaticBackend )
4873 return
74+ // An update the server just sent needs no reflection back to it.
75+ if ( remoteSyncIds . has ( syncId ) )
76+ return
4977 if ( patches ) {
5078 rpc . callEvent ( 'devframe:rpc:server-state:patch' , key , patches , syncId )
5179 }
0 commit comments