@@ -11,12 +11,13 @@ import type {
11
11
Target ,
12
12
} from "@typebot.io/typebot/schemas/edge" ;
13
13
import type { EdgeWithTotalVisits , TotalAnswers } from "../schemas" ;
14
+ import type {
15
+ DropoffLogger ,
16
+ TraversalFrame ,
17
+ VisitedPathsByEdge ,
18
+ } from "../types" ;
14
19
import { getVisitedEdgeToPropFromId } from "./getVisitedEdgeToPropFromId" ;
15
20
16
- type Logger = ( msg : string , ctx ?: Record < string , unknown > ) => void ;
17
-
18
- type Frame = { edgeId : string ; totalUsers : number ; depth : number } ;
19
-
20
21
type Params = {
21
22
initialEdge : {
22
23
id : string ;
@@ -26,7 +27,7 @@ type Params = {
26
27
edges : Edge [ ] ;
27
28
groups : GroupV6 [ ] ;
28
29
totalAnswers : TotalAnswers [ ] ;
29
- logger ?: Logger ;
30
+ logger ?: DropoffLogger ;
30
31
} ;
31
32
32
33
export function populateEdgesWithTotalVisits ( {
@@ -37,97 +38,114 @@ export function populateEdgesWithTotalVisits({
37
38
totalAnswers,
38
39
logger,
39
40
} : Params ) : EdgeWithTotalVisits [ ] {
40
- const edgesById = new Map ( edges . map ( ( e ) => [ e . id , e ] ) ) ;
41
- const groupsById = new Map ( groups . map ( ( g ) => [ g . id , g ] ) ) ;
41
+ const edgesById = new Map ( edges . map ( ( edge ) => [ edge . id , edge ] ) ) ;
42
+ const groupsById = new Map ( groups . map ( ( group ) => [ group . id , group ] ) ) ;
42
43
const totalAnswersByInputBlockId = new Map (
43
- totalAnswers . map ( ( t ) => [ t . blockId , t . total ] ) ,
44
+ totalAnswers . map ( ( answer ) => [ answer . blockId , answer . total ] ) ,
44
45
) ;
45
46
46
- const offDefaultPathEdgeIds = new Set (
47
- offDefaultPathEdgeWithTotalVisits . map ( ( e ) => e . id ) ,
47
+ const offPathEdgeIds = new Set (
48
+ offDefaultPathEdgeWithTotalVisits . map ( ( offPathEdge ) => offPathEdge . id ) ,
48
49
) ;
49
- const totals = new Map < string , number > (
50
- offDefaultPathEdgeWithTotalVisits . map ( ( e ) => [ e . id , e . total ] ) ,
50
+ const edgeTotalsById = new Map (
51
+ offDefaultPathEdgeWithTotalVisits . map ( ( offPathEdge ) => [
52
+ offPathEdge . id ,
53
+ offPathEdge . total ,
54
+ ] ) ,
51
55
) ;
52
56
53
- const visited = new Set < string > ( ) ;
57
+ const visitedByEdge : VisitedPathsByEdge = new Map ( ) ;
54
58
55
- const stack : Frame [ ] = [
56
- { edgeId : initialEdge . id , totalUsers : initialEdge . total , depth : 0 } ,
59
+ const depthFirstFrames : TraversalFrame [ ] = [
60
+ {
61
+ edgeId : initialEdge . id ,
62
+ usersRemaining : initialEdge . total ,
63
+ pathIndex : 0 ,
64
+ } ,
57
65
] ;
58
66
59
- while ( stack . length ) {
60
- const { edgeId, totalUsers, depth } = stack . pop ( ) ! ;
67
+ while ( depthFirstFrames . length ) {
68
+ visitFrame ( depthFirstFrames . pop ( ) ! ) ;
69
+ }
70
+
71
+ return [ ...edgeTotalsById . entries ( ) ] . map ( ( [ id , total ] ) => ( {
72
+ id,
73
+ total,
74
+ to : getVisitedEdgeToPropFromId ( id , { edges } ) ,
75
+ } ) ) ;
76
+
77
+ /* ================================================================ */
78
+ /* Inner helpers */
79
+ /* ================================================================ */
80
+ function visitFrame ( { edgeId, usersRemaining, pathIndex } : TraversalFrame ) {
81
+ if ( usersRemaining <= 0 ) return ;
61
82
62
- if ( totalUsers <= 0 ) continue ;
83
+ if ( markVisited ( visitedByEdge , edgeId , pathIndex ) ) return ;
63
84
64
- if ( ! offDefaultPathEdgeIds . has ( edgeId ) ) {
65
- totals . set ( edgeId , ( totals . get ( edgeId ) ?? 0 ) + totalUsers ) ;
85
+ if ( ! offPathEdgeIds . has ( edgeId ) ) {
86
+ edgeTotalsById . set (
87
+ edgeId ,
88
+ ( edgeTotalsById . get ( edgeId ) ?? 0 ) + usersRemaining ,
89
+ ) ;
66
90
}
67
91
68
- if ( visited . has ( edgeId ) ) continue ;
69
- visited . add ( edgeId ) ;
70
-
71
92
logger ?.(
72
93
`▶︎ visiting ${ edgeIdToHumanReadableLabel ( edgeId , {
73
94
edges,
74
95
groups,
75
96
offDefaultPathEdgeWithTotalVisits,
76
97
} ) } `,
77
98
{
78
- totalUsers,
79
- depth,
99
+ usersRemaining,
80
100
} ,
81
101
) ;
82
102
83
103
const edge = edgesById . get ( edgeId ) ;
84
- if ( ! edge ?. to ) continue ;
104
+ if ( ! edge ?. to ) return ;
85
105
86
106
const group = groupsById . get ( edge . to . groupId ) ;
87
- if ( ! group ) continue ;
107
+ if ( ! group ) return ;
88
108
89
- let remainingForNextDefaultOutgoingEdge = totalUsers ;
109
+ let remainingForNextDefaultOutgoingEdge = usersRemaining ;
90
110
111
+ let nextPathIndexIncrement = 1 ;
91
112
for ( const block of sliceFrom ( group . blocks , edge . to . blockId ) ) {
92
- if ( isInputBlock ( block ) )
113
+ if ( isInputBlock ( block ) ) {
93
114
remainingForNextDefaultOutgoingEdge =
94
115
totalAnswersByInputBlockId . get ( block . id ) ?? 0 ;
116
+ totalAnswersByInputBlockId . delete ( block . id ) ;
117
+ }
95
118
96
119
for ( const itemEdgeId of outgoingItemEdges ( block ) ) {
97
- const itemTotal = totals . get ( itemEdgeId ) ;
98
- if ( itemTotal ) {
99
- enqueue ( itemEdgeId , itemTotal , depth + 1 ) ;
120
+ const itemTotal = edgeTotalsById . get ( itemEdgeId ) ;
121
+ if ( itemTotal && itemTotal > 0 ) {
122
+ enqueue ( itemEdgeId , itemTotal , pathIndex + nextPathIndexIncrement ) ;
123
+ nextPathIndexIncrement ++ ;
100
124
remainingForNextDefaultOutgoingEdge -= itemTotal ;
101
125
}
102
126
}
103
127
104
128
if ( isJump ( block ) ) {
105
129
const virtualId = createVirtualEdgeId ( block . options ) ;
106
- const virtualTotal = totals . get ( virtualId ) ;
107
- if ( virtualTotal ) {
108
- enqueue ( virtualId , virtualTotal , depth + 1 ) ;
130
+ const virtualTotal = edgeTotalsById . get ( virtualId ) ;
131
+ if ( virtualTotal && virtualTotal > 0 ) {
132
+ enqueue ( virtualId , virtualTotal , pathIndex + 1 ) ;
109
133
}
110
134
}
111
135
112
136
if ( block . outgoingEdgeId ) {
113
137
enqueue (
114
138
block . outgoingEdgeId ,
115
139
remainingForNextDefaultOutgoingEdge ,
116
- depth + 1 ,
140
+ pathIndex ,
117
141
) ;
118
142
}
119
143
}
120
144
}
121
145
122
- return [ ...totals . entries ( ) ] . map ( ( [ id , total ] ) => ( {
123
- id,
124
- total,
125
- to : getVisitedEdgeToPropFromId ( id , { edges } ) ,
126
- } ) ) ;
127
-
128
- function enqueue ( id : string , totalUsers : number , depth : number ) {
129
- if ( totalUsers <= 0 || visited . has ( id ) ) return ;
130
- stack . push ( { edgeId : id , totalUsers, depth } ) ;
146
+ function enqueue ( edgeId : string , usersRemaining : number , pathIndex : number ) {
147
+ if ( usersRemaining <= 0 ) return ;
148
+ depthFirstFrames . push ( { edgeId, usersRemaining, pathIndex } ) ;
131
149
}
132
150
}
133
151
@@ -136,10 +154,11 @@ const sliceFrom = (blocks: Block[], startId?: string) =>
136
154
137
155
const outgoingItemEdges = ( block : Block ) => {
138
156
if ( ! blockHasItems ( block ) ) return [ ] ;
139
- return (
140
- block . items ?. flatMap ( ( i ) => ( i . outgoingEdgeId ? [ i . outgoingEdgeId ] : [ ] ) ) ??
141
- [ ]
142
- ) ;
157
+ const ids : string [ ] = [ ] ;
158
+ for ( const item of block . items ?? [ ] ) {
159
+ if ( item . outgoingEdgeId ) ids . push ( item . outgoingEdgeId ) ;
160
+ }
161
+ return ids ;
143
162
} ;
144
163
145
164
const isJump = (
@@ -191,3 +210,18 @@ const edgeIdToHumanReadableLabel = (
191
210
label += "]" ;
192
211
return label ;
193
212
} ;
213
+
214
+ const markVisited = (
215
+ visitedByEdge : VisitedPathsByEdge ,
216
+ edgeId : string ,
217
+ pathIdx : number ,
218
+ ) : boolean => {
219
+ let paths = visitedByEdge . get ( edgeId ) ;
220
+ if ( ! paths ) {
221
+ paths = new Set < number > ( ) ;
222
+ visitedByEdge . set ( edgeId , paths ) ;
223
+ }
224
+ if ( paths . has ( pathIdx ) ) return true ;
225
+ paths . add ( pathIdx ) ;
226
+ return false ;
227
+ } ;
0 commit comments