I do not use Ensemble, but I would try using the JSON-Adaptor, something like this

Class MessageB Extends (Ens.Request, %JSON.Adaptor)
{
   Property ClientId As %String(MAXLEN = "");
   Property message As %Stream.TmpBinary;
}

For example

s r=##class(MessageB).%New()
s r.ClientId=12345
d r.message.Write("part1")
d r.message.Write("part2")
w r.%JSONExportToStream(.s)
d s.Rewind()
w s.Read(s.Size) --> {"ClientId":"12345","message":"cGFydDFwYXJ0Mg=="}

Your solution is nearly perfect, here my quick (untested) version.


ClassMethod Encode()
{
	// You read N bytes (which MUST be divisible by 3) and write N*4/3 encoded bytes
	// 3 * 8190 = 24570; 24570 * 4 / 3 = 32760;  32760 < 32768; to avoid (slow) long strings
	set CHUNK=24570
	set NOCR=1	// don't insert CRLF after each 72 written bytes
	set encodedData=##class(%Stream.TmpBinary).%New() // adapt this to your needs: %Stream.Whatever...
	
	set request=##class(%Net.HttpRequest).%New()
	set request.Server="..."
    do request.Get("/...")
    
    if request.HttpResponse.StatusCode = 200 {
    	while 'request.HttpResponse.Data.AtEnd {
	    	do encodedData.Write($system.Encryption.Base64Encode(request.HttpResponse.Data.Read(CHUNK),1))
		}
	}
    QUIT encodedData
    
    // as an alternative, you could return a string or a streamobject
    set YOURMAXSTRING = 32767 // or 3641144
    if encodedData.Size <= YOURMAXSTRING {
	    do encodedData.Rewind()
	    quit encodedData.Read(encodedData.Size)
    } else { quit encodedData }
}

Assuming, fields which contains commas are quoted ("aaa,bbb,ccc") and (for the simplicity) fields does not contains quotes, then something like this should do the job


ClassMethod CSV(filename)
{
	s old=$system.Process.SetZEOF(1)	// use $zeof instead of error trap
	s result=[]
	o filename:"r":0
	i $t {
		u filename
		while '$zeof {
			read line
			i line]"" do result.%Push(..fields(line)) // ignore empty lines
		}
	}
	c filename
	d $system.Process.SetZEOF(old)
	q result
}

ClassMethod fields(line)
{
	s a="", f=0, row=[]
	f i=1:1:$l(line) {
		s c=$a(line,i)
		i c=44,'f d row.%Push(a) s a="" continue
		i c=34 s f='f continue
		s a=a_$c(c)
	}
	q row
}

A test output:


USER>s res=##class(DC.Help).CSV(fn)

USER>zso res
(0).(0).............: 12162
(0).(1).............: CHAPTER I
(0).(2).............: Certain infectious and parasitic diseases (A00-B99)
(0).(3).............: 003 (A20-A28)
(0).(4).............: Certain zoonotic bacterial diseases
(0).(5).............: A28
(0).(6).............: Other zoonotic bacterial diseases, not elsewhere classified
(0).(7).............: A28
(0).(8).............: Other zoonotic bacterial diseases, not elsewhere classified
(0).(9).............: N
(0).(10)............: N
(0).(11)............: N
(0).(12)............: N
(0).(13)............: N
(0).(14)............:
(0).(15)............:
(0).(16)............:
(0).(17)............:
(0).(18)............:
(0).(19)............:
(0).(20)............:
(0).(21)............:
(0).(22)............:

You are mixing two different things...

Property Data1 As list of %String;
Property Data2 As %List;

are two very different things. The first (Data1, equates to your DataObj.Services) is an object while the second one (Data2) is a simple scalar value (in this case a string which in its structure casually matches the inner structure of a $list() respective $listbuild() function).

write $listvalid(oref.Data1) ==> 0 // NOT a list
write $listvalid(oref.Data2) ==> 1 // a VALID list
write $isobject(oref.Data1) ==> 1  // a valid (list)object
write $isobject(oref.Data2) ==> 0  // not a valid (list)object

$listnext() does NOT work on objects (your DataObj.Services) is an object

For a string like "hallo" Cache will use 5+2 = 7 bytes. If that "hallo..." is longer then 253 bytes then length_of_string + 4 bytes will be used and if your "hallo..." is longer then 65535 bytes then length_of_string + 6 bytes will be used.

But there is one more thing, you should know: the sum of the lengths of ALL properties, except the array(like) properties, can't be greater then that famous 3641144 magic number (if you use the standard Cache Storage). Array-like properties are those, which are stored in own nodes.

The documentation of the %ToJSON() method is correct and yes, you can do 

 do obj.%ToJSON()
 

merely, this works only "on devices without protocol" like terminal, (sequential) file, etc. Everywhere, wehere the data bytes goes direct to the target. WebSocket isn't such a device. There is a "header part", with information fields like the command, masking, the length of the data, etc.

You have two possibilities, a) you ask WRC for a "WriteStream()" method or b) you handle the whole WebSocket by hand (is not impossible) or c) you change your application logic and send the those messages in chunks.

Yes, it would

kill
set $p(data,"x",2E6+1)=""
set dynArr=[].%Push(data).%Push(data).%Push(data)
set stream=##class(%Stream.GlobalCharacter).%New()
do dynArr.%ToJSON(stream)
try { set x=dynArr.%ToJSON() } catch e { write e.Name,! }
write stream.Size,!
write $length(data),!

The output is

<MAXSTRING>
6000010
2000000

As Steven Hobbs  wrote, the limit is only set by the size of the (virtual) memory.