|
208 | 208 | # hop and basn's streamcall WS fan-out. ``MIN_VERSION`` |
209 | 209 | # tracks ``BACLOUD_VERSION`` as usual; prod and |
210 | 210 | # push-public land together. |
211 | | -BACLOUD_VERSION = 28 |
| 211 | +# 29 (2026-08): Responses larger than one SmartSocket message are |
| 212 | +# split across several. Adds ``ResponseTypeID.CHUNK`` |
| 213 | +# and ``ChunkedResponse``: the session sender slices a |
| 214 | +# too-large serialized response into ordered pieces and |
| 215 | +# the client rejoins them before decoding. The transport |
| 216 | +# caps a single message deliberately (they are |
| 217 | +# load-bearing for the relay's resend-buffer and linger |
| 218 | +# math), so a big response has to be split *above* it -- |
| 219 | +# previously nothing did, and a ~1.5 MB vendored asset |
| 220 | +# package was simply retained and retried by the relay |
| 221 | +# forever, presenting as a half-hour hang. ``MIN_VERSION`` |
| 222 | +# tracks ``BACLOUD_VERSION`` as usual, so older clients |
| 223 | +# get the standard "please update" rejection rather than |
| 224 | +# an undecodable type id. |
| 225 | +BACLOUD_VERSION = 29 |
212 | 226 |
|
213 | 227 |
|
214 | 228 | def asset_file_cache_path(filehash: str) -> str: |
@@ -281,6 +295,7 @@ class ResponseTypeID(Enum): |
281 | 295 | STANDARD = 's' |
282 | 296 | SESSION_HANDLE = 'sh' |
283 | 297 | STREAM_OUTPUT = 'so' |
| 298 | + CHUNK = 'ch' |
284 | 299 |
|
285 | 300 |
|
286 | 301 | class ResponseData(IOMultiType[ResponseTypeID]): |
@@ -320,6 +335,9 @@ def get_type(cls, type_id: ResponseTypeID) -> type[ResponseData]: |
320 | 335 | if type_id is ResponseTypeID.STREAM_OUTPUT: |
321 | 336 | out = StreamOutputResponse |
322 | 337 | return out |
| 338 | + if type_id is ResponseTypeID.CHUNK: |
| 339 | + out = ChunkedResponse |
| 340 | + return out |
323 | 341 | raise ValueError(f'Unrecognized type-id {type_id}.') |
324 | 342 |
|
325 | 343 |
|
@@ -962,6 +980,44 @@ def get_type_id(cls) -> ResponseTypeID: |
962 | 980 | return ResponseTypeID.STREAM_OUTPUT |
963 | 981 |
|
964 | 982 |
|
| 983 | +@ioprepped |
| 984 | +@dataclass |
| 985 | +class ChunkedResponse(ResponseData): |
| 986 | + """One ordered slice of a response too large for a single message. |
| 987 | +
|
| 988 | + The transport caps a single message on purpose -- that cap is what |
| 989 | + the relay's resend-buffer and linger math are sized against, so it |
| 990 | + does not grow to fit one caller's payload. A response past it is |
| 991 | + therefore split here, above the transport, and rejoined by the |
| 992 | + reader before it decodes anything. |
| 993 | +
|
| 994 | + No correlation id and no total-length field: the session delivers |
| 995 | + gaplessly and in order (that is its whole invariant), and a sender |
| 996 | + finishes one response before starting the next, so "collect until |
| 997 | + ``index == count - 1``" is sufficient and cannot interleave. A |
| 998 | + reader that sees a gap has already lost the session. |
| 999 | +
|
| 1000 | + ``data`` is a slice of the *serialized* response, not a serialized |
| 1001 | + slice -- rejoining is string concatenation, and the result decodes |
| 1002 | + exactly as it would have unsplit. That keeps every existing |
| 1003 | + response type carryable with no per-type awareness here. |
| 1004 | + """ |
| 1005 | + |
| 1006 | + #: Position of this slice, from zero. |
| 1007 | + index: Annotated[int, IOAttrs('i')] |
| 1008 | + |
| 1009 | + #: How many slices the whole response was split into. |
| 1010 | + count: Annotated[int, IOAttrs('n')] |
| 1011 | + |
| 1012 | + #: This slice of the serialized response. |
| 1013 | + data: Annotated[str, IOAttrs('d')] |
| 1014 | + |
| 1015 | + @override |
| 1016 | + @classmethod |
| 1017 | + def get_type_id(cls) -> ResponseTypeID: |
| 1018 | + return ResponseTypeID.CHUNK |
| 1019 | + |
| 1020 | + |
965 | 1021 | @ioprepped |
966 | 1022 | @dataclass |
967 | 1023 | class SessionHandleResponse(ResponseData): |
|
0 commit comments