582
]
583
)
584
585
+ results = self._collect_results(
586
+ executions, event_type, correlation_id, skip_none=True,
587
+ )
588
+
589
+ await self._publish_diagnostic_event(
590
+ lambda: {
591
+ "kind": "inbound",
592
+ "sourceNamespace": namespace,
593
+ "namespace": namespace,
594
+ "eventType": event_type,
595
+ "sid": sid,
596
+ "correlationId": correlation_id,
597
+ "timestamp": self._timestamp(),
598
+ "handlerCount": len(handlers),
599
+ "durationMs": sum(
600
+ (exec.duration_ms or 0.0) for exec in executions
601
+ ),
602
+ "resultSummary": self._summarize_results(results),
603
+ "payloadSummary": self._summarize_payload(handler_payload),
604
+ }
605
+ )
606
+
607
+ response = {"correlationId": correlation_id, "results": results}
608
+ self._debug(
609
+ f"Completed client event namespace={namespace} '{event_type}' "
610
+ f"sid={sid} correlation={correlation_id}"
611
+ )
612
+ return response
613
+
614
+ def _collect_results(
615
+ self,
616
+ executions: list[_HandlerExecution],
617
+ event_type: str,
618
+ correlation_id: str,
619
+ *,
620
+ skip_none: bool = False,
621
+ ) -> List[dict[str, Any]]:
622
+ """Build a result list from handler executions.
623
+
624
+ Args:
625
+ skip_none: When ``True``, handlers that return ``None`` are omitted
626
+ from the results (fire-and-forget / opt-out semantics used by
627
+ ``process_client_event``). When ``False``, ``None`` is converted
628
+ to ``WsResult(ok=True)`` (default ``route_event`` behaviour).
629
+ """
630
results: List[dict[str, Any]] = []
631
for execution in executions:
632
handler = execution.handler
660
)
661
continue
662
618
- # Skip handlers that return None — they opted out of contributing
619
- # a result (fire-and-forget semantics, matching legacy _dispatch).
663
if value is None:
621
- continue
622
-
623
- if isinstance(value, dict):
664
+ if skip_none:
665
+ continue
666
+ helper_result = WsResult(ok=True)
667
+ elif isinstance(value, dict):
668
helper_result = WsResult(ok=True, data=value)
669
else:
670
helper_result = WsResult(ok=True, data={"result": value})
676
duration_ms=duration_ms,
677
)
678
)
635
-
636
- await self._publish_diagnostic_event(
637
- lambda: {
638
- "kind": "inbound",
639
- "sourceNamespace": namespace,
640
- "namespace": namespace,
641
- "eventType": event_type,
642
- "sid": sid,
643
- "correlationId": correlation_id,
644
- "timestamp": self._timestamp(),
645
- "handlerCount": len(handlers),
646
- "durationMs": sum(
647
- (exec.duration_ms or 0.0) for exec in executions
648
- ),
649
- "resultSummary": self._summarize_results(results),
650
- "payloadSummary": self._summarize_payload(handler_payload),
651
- }
652
- )
653
-
654
- response = {"correlationId": correlation_id, "results": results}
655
- self._debug(
656
- f"Completed client event namespace={namespace} '{event_type}' "
657
- f"sid={sid} correlation={correlation_id}"
658
- )
659
- return response
679
+ return results
680
681
async def _invoke_handler(
682
self,
727
sid,
728
"server_restart",
729
{
710
- "emittedAt": _utcnow()
711
- .isoformat(timespec="milliseconds")
712
- .replace("+00:00", "Z"),
730
+ "emittedAt": self._timestamp(),
731
"runtimeId": runtime.get_runtime_id(),
732
},
733
handler_id=self._identifier,
960
]
961
)
962
945
- results: List[dict[str, Any]] = []
946
- for execution in executions:
947
- handler = execution.handler
948
- value = execution.value
949
- duration_ms = execution.duration_ms
950
-
951
- if isinstance(value, Exception): # pragma: no cover - defensive logging
952
- PrintStyle.error(
953
- f"Error in handler {handler.identifier} for '{event_type}' (correlation {correlation_id}): {value}"
954
- )
955
- results.append(
956
- self._build_error_result(
957
- handler_id=handler.identifier,
958
- code="HANDLER_ERROR",
959
- message="Internal server error",
960
- details=str(value),
961
- correlation_id=correlation_id,
962
- duration_ms=duration_ms,
963
- )
964
- )
965
- continue
966
-
967
- if isinstance(value, WsResult):
968
- results.append(
969
- value.as_result(
970
- handler_id=handler.identifier,
971
- fallback_correlation_id=correlation_id,
972
- duration_ms=duration_ms,
973
- )
974
- )
975
- continue
976
-
977
- if value is None:
978
- helper_result = WsResult(ok=True)
979
- elif isinstance(value, dict):
980
- helper_result = WsResult(ok=True, data=value)
981
- else:
982
- helper_result = WsResult(ok=True, data={"result": value})
983
-
984
- results.append(
985
- helper_result.as_result(
986
- handler_id=handler.identifier,
987
- fallback_correlation_id=correlation_id,
988
- duration_ms=duration_ms,
989
- )
990
- )
963
+ # NOTE: skip_none=False here — route_event converts None to ok=True,
964
+ # unlike process_client_event which skips None (fire-and-forget).
965
+ # This is intentional: route_event is server-initiated and callers
966
+ # expect a result entry for every handler.
967
+ results = self._collect_results(
968
+ executions, event_type, correlation_id, skip_none=False,
969
+ )
970
971
await self._publish_diagnostic_event(
972
lambda: {
1200
correlation_id: str | None = None,
1201
) -> dict[str, Any]:
1202
hid = handler_id or self._identifier
1224
- ts = _utcnow().isoformat(timespec="milliseconds").replace("+00:00", "Z")
1203
+ ts = self._timestamp()
1204
event_id = str(uuid.uuid4())
1205
correlation = correlation_id or str(uuid.uuid4())
1206
return {