11import { useEffect , useMemo , useRef } from "react" ;
22import { eventHub } from "../../../shared/signalr" ;
33import { useAuth } from "../../../features/auth" ;
4- import type { JsonValue } from "../../../shared/types/json" ;
4+ import { isJsonObject , type JsonValue } from "../../../shared/types/json" ;
55
66interface UseFilesRealtimeEventsOptions {
77 nodeId : string | null ;
@@ -22,6 +22,110 @@ const FILES_HUB_METHODS = [
2222] as const ;
2323
2424const PREVIEW_GENERATED_METHOD = "PreviewGenerated" ;
25+ const GUID_REGEX =
26+ / ^ [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 1 - 5 ] [ 0 - 9 a - f ] { 3 } - [ 8 9 a b ] [ 0 - 9 a - f ] { 3 } - [ 0 - 9 a - f ] { 12 } $ / i;
27+ const MAX_PAYLOAD_SCAN_DEPTH = 4 ;
28+
29+ const normalizeKey = ( key : string ) : string =>
30+ key . replace ( / [ ^ a - z ] / gi, "" ) . toLowerCase ( ) ;
31+
32+ const looksLikeNodeRelationKey = ( key : string ) : boolean => {
33+ const normalized = normalizeKey ( key ) ;
34+
35+ if (
36+ normalized === "node" ||
37+ normalized === "parent" ||
38+ normalized === "folder"
39+ ) {
40+ return true ;
41+ }
42+
43+ return (
44+ normalized . includes ( "nodeid" ) ||
45+ normalized . includes ( "parentid" ) ||
46+ normalized . includes ( "folderid" ) ||
47+ normalized . includes ( "sourceid" ) ||
48+ normalized . includes ( "targetid" ) ||
49+ normalized . includes ( "destinationid" ) ||
50+ normalized . includes ( "fromid" ) ||
51+ normalized . includes ( "toid" )
52+ ) ;
53+ } ;
54+
55+ const isGuid = ( value : string ) : boolean => GUID_REGEX . test ( value ) ;
56+
57+ const collectAffectedNodeIdsFromValue = (
58+ value : JsonValue ,
59+ depth : number ,
60+ relationContext : boolean ,
61+ ) : string [ ] => {
62+ if ( depth > MAX_PAYLOAD_SCAN_DEPTH ) {
63+ return [ ] ;
64+ }
65+
66+ if ( typeof value === "string" ) {
67+ return relationContext && isGuid ( value ) ? [ value ] : [ ] ;
68+ }
69+
70+ if ( Array . isArray ( value ) ) {
71+ return value . flatMap ( ( entry ) =>
72+ collectAffectedNodeIdsFromValue ( entry , depth + 1 , relationContext ) ,
73+ ) ;
74+ }
75+
76+ if ( ! isJsonObject ( value ) ) {
77+ return [ ] ;
78+ }
79+
80+ const ids : string [ ] = [ ] ;
81+
82+ for ( const [ key , nested ] of Object . entries ( value ) ) {
83+ const nextRelationContext = relationContext || looksLikeNodeRelationKey ( key ) ;
84+ ids . push (
85+ ...collectAffectedNodeIdsFromValue (
86+ nested ,
87+ depth + 1 ,
88+ nextRelationContext ,
89+ ) ,
90+ ) ;
91+ }
92+
93+ return ids ;
94+ } ;
95+
96+ const collectAffectedNodeIds = ( args : JsonValue [ ] ) : Set < string > => {
97+ const ids = new Set < string > ( ) ;
98+
99+ for ( const arg of args ) {
100+ if ( typeof arg === "string" && isGuid ( arg ) ) {
101+ ids . add ( arg ) ;
102+ }
103+
104+ const nestedIds = collectAffectedNodeIdsFromValue ( arg , 0 , false ) ;
105+ for ( const id of nestedIds ) {
106+ ids . add ( id ) ;
107+ }
108+ }
109+
110+ return ids ;
111+ } ;
112+
113+ const shouldInvalidateCurrentNode = (
114+ args : JsonValue [ ] ,
115+ currentNodeId : string | null ,
116+ ) : boolean => {
117+ if ( ! currentNodeId ) {
118+ return false ;
119+ }
120+
121+ const affectedNodeIds = collectAffectedNodeIds ( args ) ;
122+ if ( affectedNodeIds . size === 0 ) {
123+ // Keep compatibility with events that do not include node identifiers.
124+ return true ;
125+ }
126+
127+ return affectedNodeIds . has ( currentNodeId ) ;
128+ } ;
25129
26130const isPreviewGeneratedArgs = (
27131 args : JsonValue [ ] ,
@@ -85,7 +189,11 @@ export function useFilesRealtimeEvents({
85189
86190 const invalidationMethods = FILES_HUB_METHODS . flatMap ( ( m ) => [ m , m . toLowerCase ( ) ] ) ;
87191 const unsubscribes = invalidationMethods . map ( ( method ) =>
88- eventHub . on ( method , ( ) => {
192+ eventHub . on ( method , ( ...args : JsonValue [ ] ) => {
193+ if ( ! shouldInvalidateCurrentNode ( args , nodeIdRef . current ) ) {
194+ return ;
195+ }
196+
89197 scheduleInvalidate ( ) ;
90198 } ) ,
91199 ) ;
0 commit comments